2

I have such a simple checkmark SwiftUI view. It is displayed in a List and can be toggled by tapping on it or can be toggled when refreshed from a data source (ex. Core Data)

My First implementation was

struct RoundedCheckmark: View {

    @State var isChecked : Bool
    let onCheck: (Bool) -> Void

    func toggle() { isChecked = !isChecked; onCheck(isChecked) }

    init(isChecked: Bool, onCheck: @escaping (Bool) -> Void = { _ in }) {
        self._isChecked = State(initialValue: isChecked)
        self.onCheck = onCheck
    }

    var body: some View {

        Button(action: toggle) {

            Image(isChecked ? "CheckedCheckmark" : "UncheckedCheckmark")
        }
    }
}

It seemed to work I could toggle it and it loads correctly. On toggle, I saved changed via onCheck closure/callback and it refreshed ok.

But after external refreshes like push notification refreshing underlying model in Core Data refresh of this View doesn't correctly change @State property isChecked.

In init() I am getting new value of isChecked and reinitialized @State with State(initialValue: ). But in the body, I am getting the old value of isChecked as from old @State property. so the view does have the wrong image.

Here is workaround i.e. relying only on let isChecked property and onCheck callback. But now I cannot have state inside my View, and I have to only rely on external data source change. Maybe it is more appropriate as this way I have a single source of truth without local @State storage.

struct RoundedCheckmark: View {

    let isChecked: Bool
    let onCheck: (Bool) -> Void

    func toggle() { onCheck(isChecked) }

    init(isChecked: Bool, onCheck: @escaping (Bool) -> Void = { _ in }) {

        self.isChecked = isChecked
        self.onCheck = onCheck
    }

    var body: some View {

        Button(action: toggle) {

            Image(isChecked ? "CheckedCheckmark" : "UncheckedCheckmark")

        }

    }
}
sandpat
  • 1,478
  • 12
  • 30
Michał Ziobro
  • 10,759
  • 11
  • 88
  • 143
  • Looks like you answered your own question... – Jacob Relkin Feb 26 '20 at 11:24
  • Yes but I do not understand why first solution does not work. It is that with each instance of the View there is new @State created but View is using old one? If so then how I can change this state (and whether I should try to change it?) Do you mean this why I will have two sources of truth? – Michał Ziobro Feb 26 '20 at 11:44

1 Answers1

1

isChecked as a @State variable is your source of truth. That means when some underlying data model (like CoreData) changes, it doesn't matter. Your view is only looking at the local @State version of isChecked.

But look at your view. To me, it shouldn't own its own state. Why? Because there is no semantic meaning to this view as a checkmark view. Its parent appears to own the state (hence why there is a onCheck callback). Instead, this should use a @Binding with no callback:

struct RoundedCheckmark: View {
    @Binding var isChecked: Bool

    var body: some View {
        Button(action: { isChecked.toggle() }) {
            Image(isChecked ? "CheckedCheckmark" : "UncheckedCheckmark")
        }
    }

Now your parent owns the state and you can infer its semantic meaning:

struct CheckmarkOwner: View {
    @State var showFavoritesOnly = false

    var body: some View {
        // content

        RoundedCheckmark(isChecked: $showFavoritesOnly)

        // and now something else will get notified when `showFavoritesOnly` gets toggled
        if showFavoritesOnly {
             // toggleable content
        }

        // more content
    }
}
Procrastin8
  • 4,193
  • 12
  • 25
  • I've tested it and in other places it works i.e. if I pass some external date to init() than use them to initialize @State with State(initialValue: ). Then if that data refreshes in external source then init() is called again, and State(initialValue: ) is reinitialized and it usually works ok. There are also case when this external data is Core Data managed object, then it is important to know that this object is like scratch pad nad sometimes we can change it and think that after refresh it doesn't update correctly but it's state was not rolled back using ManagedObjectContext.rollBack – Michał Ziobro Feb 28 '20 at 17:24