Search code examples
iosswiftswiftuicore-datastate

Why does Binding of CoreData NSManagedObject not update onChange in Swift?


Context

I have a pretty simple Form using the MVVM architecture. However, I encountered a problem with this implementation. My DetailView has a DummyState that changes the corresponding attribute of Entity onChange.

Problem: Once the DetailView appears, both Toggles are on which is as expected. However, when turning the local Toggle off, the other Toggle remains on, which is unexpected. However, as soon as I interact with another element (not in MRE), the second Toggle seems to refresh and turns off.


Code

@objc(Entity) public class Entity: NSManagedObject {
    @NSManaged public var enabled: Bool
}

class FormViewModel: ObservableObject, CustomFormObservable {
    @Published var entity = Entity(context: ...)

    init() { self.entity.enabled = true }

    var isValid: Bool { self.entity.enabled }
}

struct FormView: View {
    @StateObject private var formVM = FormViewModel()

    var body: some View {
        CustomForm(formVM: formVM) {
            DetailView(entity: $formVM.entity)
        }
    }
}

struct DetailView: View {
    @Binding var entity: Entity
    @State private var localEnabled: Bool = true

    var body: some View {
        Toggle("Local Toggle", isOn: $localEnabled)
            .onChange(of: localEnabled) { entity.enabled = $0 }

        Toggle("Toggle", isOn: $entity.enabled)
    }
}

Question

  • What causes this behaviour and how can I solve it?

Solution

  • Because NSManagedObject is reference type.

    There is a simple rule:

    • @State and @Binding is for value types like structs.
    • @StateObject and @ObservedObject is for reference types like classes.

    And the dollar-sign reference is also only for value types.

    Fortunately NSManagedObject conforms to ObservableObjectby default.


    struct MainView: View {
        @StateObject private var formVM = FormViewModel()
    
        var body: some View {
            DetailView(entity: formVM.entity)
        }
    }
    
    struct DetailView: View {
        @ObservedObject var entity: Entity
        @State private var localEnabled: Bool = true
    
        var body: some View {
            Toggle("Local Toggle", isOn: $localEnabled)
                .onChange(of: localEnabled) { entity.enabled = $0 }
    
            Toggle("Toggle", isOn: $entity.enabled)
        }
    }