Home > Software design >  SwiftUI avoid repeated onChange call
SwiftUI avoid repeated onChange call

Time:10-13

In my code, the "final" toggle's value is based on the complex logic inside the view model. For example, user turns the toggle on but the logic can turn it off if certain conditions are not satisfied (here below simplified to number comparison). Problem: after user turns the toggle on, onChange is fired and if the logic finds out it should be turned off, the onChange will be fired once again because there is a change (this time not made by the user).

For example, if the generated random number is 5, then the console prints the following statements (8 comes from the second call):

    on change called
    shouldUseToggle called
    5
    on change called
    shouldUseToggle called
    8
    onChange(of: Bool) action tried to update multiple times per frame.

I would like to avoid it and make onChange react only to the user's change, not the change that comes from the view model. Is there a way to do this ? Or maybe other way to solve it ?

import SwiftUI

struct ContentView: View {
    @StateObject var myViewModel = MyViewModel()
    
    var body: some View {
        VStack(spacing: 0) {
            Toggle("Use xyz ?", isOn: $myViewModel.turnToggleOn).onChange(of: myViewModel.turnToggleOn, perform: { userTurnedOn in
                print("on change called")
                myViewModel.shouldUseToggle(userTurnedOn: userTurnedOn)
            })
        }
    }
}

class MyViewModel: ObservableObject {
    @Published var turnToggleOn = false
    
    func shouldUseToggle(userTurnedOn: Bool) {
        ///some complex logic comes here, for simplicity I use random numbers
        print("shouldUseToggle called")
        let x = Int.random(in: 0..<10)
        print(x)
        if userTurnedOn {
            turnToggleOn = x > 5
        } else {
            turnToggleOn = x > 3
        }
    }
}

CodePudding user response:

Use custom binding and call your logic function direct from binding.

struct ContentView: View {
    
    @StateObject var myViewModel = MyViewModel()
    
    var body: some View {
        VStack(spacing: 0) {
            Toggle("Use xyz ?", isOn: Binding(get: {myViewModel.turnToggleOn}, set: {
                myViewModel.turnToggleOn = $0;
                myViewModel.shouldUseToggle(userTurnedOn: $0)
            }))
        }
    }
}

class MyViewModel: ObservableObject {
    var turnToggleOn = false
    
    func shouldUseToggle(userTurnedOn: Bool) {
        ///some complex logic comes here, for simplicity I use random numbers
        print("shouldUseToggle called")
        let x = Int.random(in: 0..<10)
        print(x)
        if userTurnedOn {
            turnToggleOn = x > 5
        } else {
            turnToggleOn = x > 3
        }
    }
}
  • Related