ComposableArchitecture and CoreData: what are the options?

Hi. I'm trying to get an overview of what approaches there is to integrate persistence (specifically with CoreData) with Composable Architecture. I have come up with 3 different ones. Any more strategies? Any recommendations how I should handle this? What are the pros/cons?

Which options do we have to combine CoreData with ComposableArchitecture?

  1. Use AppState and actions as normal. Changes just update the AppState. Then I have an action that can persist the AppState to CoreData. It matches ID's and update/deletes/inserts only state that has changed. This action can be triggered whenever it fits your application.
  2. When value changes, persist value in CoreData via an effect, if effect is successful, update value in AppState.
  3. Subscribe to changes to CoreData and update AppState accordingly. Value changes are sent to CoreData to be persisted via an effect. AppState is readonly, to prevent direct changes to AppState?

I'm currently using strategy #1, but I'm seeing now that is a bit difficult to manage, syncing the entire AppState at once with core data, and making sure all relationships etc. are updated accordingly. I'm working on a time tracker, which have relationships between projects→tasks→activity, so there is a lot to update when things change. That is why I'm now trying to do a more systematic overview, before I try a different approach.

What are the pros and cons of each?

  1. Pros: You can start by implementing the state regularly, and then later on add persistence without changing the state structure. Cons: Get's convoluted to keep the states in sync, difficult to maintain. The states can get out of sync, of the app state was unsuccessfully persisted. But this can be handled.
  2. Pros: Persisted state and app state will be in sync. Easier to manage since you only change and persist one piece of the state at a time. Cons: More code to maintain. Each place you wan't to change a value, you have to both persist it to CoreData, and then update app state. Persistence get's mixed in throughout the app. Not as clean separation as the first.
  3. Pros: States will be in sync. A bit less code to maintain, since you can write a more generic subscriber/state update effect. Cons: Persistence get's mixed in throughout the app. Not as clean separation as the first.

We don't use CoreData but have a similar challenge with using UserDefaults.
When using higher order reducers and keeping the data in one struct instead of multiple places, you can avoid a lot of issues.

We have an extension that's does look something like this:

extension Reducer where State == Root.State, Environment == Root.Environment, Action == Root.Action {
    func persistSettings() -> Reducer {
        return .init { state, action, environment in
            let previousState = state

            var effect = self.run(&state, action, environment)

            if state.settings != previousState.settings {
                effect = .merge(effect, Effect(value: .persistSettings(state.settings)).debounce(id: SettingsPersistanceIdentifier(), for: 1, scheduler: environment.mainQueue))
            }
            if state.user != previousState.user {
                effect = .merge(effect, Effect(value: .persistUser(state.user)).debounce(id: UserPersistanceIdentifier(), for: 1, scheduler: environment.mainQueue))
            }

            return effect
        }
    }
}

Hi @eivindml, It's been about 9 months since your original post, and I'm curious which approach you ended up using?

2 Likes

No, not yet. Just picked back up my project, and will face this issue again soon. I'm curious to see how Point-Free solves it in their Standups app.

Standups does not use Core Data. It just reads/writes Codable models to disk.

The PF guys have said they aim to ship SwiftData integration tools on/after the library integrating the @Observable macro.

(This won't change the fact that persistence is obviously very effectful and belongs in a dependency. I suspect TCA will ship a built-in SwiftData wrapper dependency, plus tools to make syncing persistence state and reducer state low-boilerplate and analogous to Apple's tools for using SwiftData right in SwiftUI. But this is just a guess and I have no foreknowledge at all!)

Thanks for the clarification. Excited to see what the SwiftData integration will look like. :blush:

A bit off topic maybe, because you didn't ask about the philosophy, but I think it's a bit confusing to use both CoreData and ComposableArchitecture in same app/functionality. Both frameworks serve same purpose - keeping state consistent. Both frameworks have an option to persist the state and restore it later. You may end up managing state twice, e.g. replicating the schema etc, without actually getting any benefit from CoreData.

By making ComposableArthitecture's State "Codable" (aka Persistable) you simply extend AppState's functionality to be storable, without introducing the whole new state management system that duplicates a lot of TCA's functionality. I do understand that Core Data may simply be an implementation detail of "AppStorage" functionality, but is there a benefit to use it under the hood of "AppStorage"?

As for you question, it's an interesting one, I've stumbled upon it many times, and I don't think there is ideal approach. Storing first and then updating state seems clear and robust to me, as it's unidirectional, and if there is an error while storing - we stop and don't proceed with updating state, and no need to recover etc. But it gives us an omnipresent cycle. The whole system now goes through this cycle when updating state.

But no matter the approach, It would be omnipresent anyway. Thus, no matter how you do it, either "store first" or "update state first", I'd hide this implementation detail somehow. So that you can change it later more easily, if you don't like it anymore. This would require the whole new layer of abstraction and you won't be able to update state directly, but at least you wouldn't be afraid to start with any of approaches.