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
        }
    }
}