tgrapperon / swift-dependencies-additions

More dependencies for `swift-dependencies`
MIT License
298 stars 39 forks source link

`_CoreDataDependency` doesn't update its `Fetched` item. #68

Open tgrapperon opened 1 year ago

tgrapperon commented 1 year ago

Discussed in https://github.com/tgrapperon/swift-dependencies-additions/discussions/67

Originally posted by **m-housh** May 19, 2023 Forgive my ignorance with `CoreData` in general, but I'm playing around with the `_CoreDataDependency` and trying to determine how to update a `Fetched` item. I have tried several approaches, mainly the following... ```swift struct TodoFeature: Reducer { struct State: Equatable { var todos: Todo.FetchedResults = .empty } Action: Equatable { ... case toggleComplete(todo: Fetched) } @Dependency(\.persistentContainer) var persistentContainer; var body: some ReducerOf { Reduce { state, action in switch action { ... case .toggleComplete(todo: let todo): todo.withManagedObject { update in update.complete.toggle() try! update.managedObjectContext!.save() } return .none } } } } ``` This does not update the view / state, however if I shut the app down and restart it, the complete value is toggled. So I'm not sure if I need to invalidate the cached items that have been fetched, but calling my task that initially loads the todo's does not seem to update the todo's state either. Once again, forgive my ignorance in working with core data in general as it seems like there's something basic that I'm missing. Here's the repository for more complete example. https://github.com/m-housh/CoreData_Test
acosmicflamingo commented 1 year ago

I used print statements to verify that state is indeed being mutated as one would expect. Since the reducer is not recognizing that anything changed at all, that leads me to believe that Fetched<ManagedObject> itself is having an issue with equatability. What's interesting is that there is never an instance when Fetched actually conforms to the Equatable protocol, but the compiler would complain when I try to explicitly conform it myself. Turns out that Hashable conforms to Equatable, so because Fetched conforms to Hashable, it will automatically conform to Equatable. I don't see any explicit overloading of ==, so Fetched must be relying on whatever the compiler automatically generates for us.

What I also find odd is that print(state.todo.first) shows me this:

Fetched(
  id: _NSCoreDataTaggedObjectID(),
  context: NSManagedObjectContext(),
  viewContext: NSManagedObjectContext(↩︎),
  token: nil
)

So strange that I don't see the object property in there, which is what holds the actual value of interest. Could it be that the synthesized version is doing a comparison of something like this?

extension Fetched: Equatable {
  public static func == (lhs: Fetched, rhs: Fetched) -> Bool {
    return lhs.id == rhs.id &&
      lhs.context == rhs.context &&
      lhs.viewContext == rhs.viewContext &&
      lhs.token == rhs.token
  }
}

That would certainly explain why despite mutating state, the reducer is not actually sending updates to ViewStore.

acosmicflamingo commented 1 year ago

Oh fascinating! Looking through the Swift proposal SE-0185 relating to synthesizing Equatable and Hashable conformance, computed properties are completely ignored:

Synthesized requirements for structs For a struct, synthesis of P's requirements is based on the conformances of only its stored instance properties. Neither static properties nor computed instance properties (those with custom getters) are considered.

Something tells me it's not a coincidence that the very property we need to check for equatability might be excluded from whatever the Swift compiler synthesizes for us ;)

acosmicflamingo commented 1 year ago

Oh no :( :( :( object as a reference type makes this so messy...you have two value types Fetched being compared, but if object is a reference type, then by the time we test for equatability, the mutation that occurred in one of the Fetched value types will be seen in the other. I have no clue how to work around that...