Search code examples
swiftswiftuiswiftdataswiftui-toggle

Toggle onChange only when interacted with


I am displaying a checklist that allows the user to check options on and off:

ForEach(checklist.items.sorted(by: { $0.sortOrder < $1.sortOrder })) { item in
    @Bindable var item = item
    Toggle(item.title, isOn: $item.isChecked)
    .toggleStyle(CheckboxToggleStyle())
    .onChange(of: item.isChecked) { newValue in
        checklistChanged()
    }
}

func checklistChanged() {
    //check some things and update other parts of the UI
}

This works fine. However, the checklist is a SwiftData @Model which has a method to reset the checklist:

func reset() {
    for item in self.items {
        item.isChecked = false
    }
}

And calling this function causes the checklistChanged() function in my view to get called (multiple times) because the isChecked property is being changed on all the items. I only want the checklistChanged() func to be called specifically when a user taps and changes a checkbox value. How can I separate these concerns? I tried using onTapGesture instead of onChange on the toggle but that doesn't get called.


Solution

  • There is no way to tell the difference between the Toggle changing isChecked, vs your code changing isChecked. You need some extra information to indicate who changed it.

    The simplest way is to just add a @Transient property to your model, to indicate whether isChecked has been programmatically changed.

    @Transient
    var programmaticallyChanged: Bool = false
    

    If you need to scale this up for all the properties in your model, you can write a member macro that is attached to your @Model class to generate these.

    You can set this in reset:

    func reset() {
        for item in self.items {
            if item.isChecked {
                item.isChecked = false
                item.programmaticallyChanged = true
            }
        }
    }
    

    Then you can check for this flag in onChange:

    .onChange(of: item.isChecked) {
        if item.programmaticallyChanged {
            item.programmaticallyChanged = false
        } else {
            checklistChanged()
        }
    }