eivindml
(Eivind LindbrÄten)
1
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?
- 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.
- When value changes, persist value in CoreData via an effect, if effect is successful, update value in AppState.
- 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.
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
eivindml
(Eivind LindbrÄten)
5
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.
kamcma
(Kyle McMahon)
6
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!)
eivindml
(Eivind LindbrÄten)
7
Thanks for the clarification. Excited to see what the SwiftData integration will look like. 
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.