swiftswiftuitoggledidset

SwiftUI: Action not triggering with toggle


I'm trying to trigger an action with a toggle switch. In this case the print message "value did change" doesn't show up on console after clicking on the toggle.

This is targeted for a macOS 10.15 app, .onChange will not work.

struct ContentView: View {
    @State private var isToggle : Bool = false {
            didSet {
                print("value did change")
            }
    }

    var body: some View {
        Toggle(isOn: self.$isToggle){
                    Text("Toggle Label ")
         }
    }
}

Solution

  • I am going to shamelessly steal the first part of @Asperi's answer, but the second part is mine...

    @State private var isToggle : Bool = false
    
    var body: some View {
        Toggle(isOn: self.$isToggle.onUpdate({
            print("value did change")   // << here !!
        })){
            Text("Toggle Label ")
        }
    }
    
    extension Binding {
        
        /// Adds a modifier for this Binding that fires an action when a specific
        /// value changes.
        ///
        /// You can use `onUpdate` to trigger a side effect as the result of a
        /// `Binding` value changing.
        ///
        /// `onUpdate` is called on the main thread. Avoid performing long-running
        /// tasks on the main thread. If you need to perform a long-running task in
        /// response to `value` changing, you should dispatch to a background queue.
        ///
        /// The new value is NOT passed into the closure.
        ///
        ///     struct PlayerView: View {
        ///         var episode: Episode
        ///         @State private var playState: PlayState = .paused
        ///
        ///         var body: some View {
        ///             VStack {
        ///                 Text(episode.title)
        ///                 Text(episode.showTitle)
        ///                 PlayButton(playState: $playState.updated {
        ///                     model.playStateDidChange.update()
        ///                 })
        ///             }
        ///         }
        ///     }
        ///
        /// - Parameters:
        ///   - action: A closure to run when the value changes.
        ///
        /// - Returns: A new binding value.
    
        func onUpdate(_ action: @escaping () -> Void) -> Binding<Value> {
            Binding(get: {
                wrappedValue
            }, set: { newValue in
                wrappedValue = newValue
                action()
            })
        }
    }
    
    extension Binding {
    
        /// Adds a modifier for this Binding that fires an action when a specific
        /// value changes.
        ///
        /// You can use `updated` to trigger a side effect as the result of a
        /// `Binding` value changing.
        ///
        /// `updated` is called on the main thread. Avoid performing long-running
        /// tasks on the main thread. If you need to perform a long-running task in
        /// response to `value` changing, you should dispatch to a background queue.
        ///
        /// The new value is passed into the closure.
        ///
        ///     struct PlayerView: View {
        ///         var episode: Episode
        ///         @State private var playState: PlayState = .paused
        ///
        ///         var body: some View {
        ///             VStack {
        ///                 Text(episode.title)
        ///                 Text(episode.showTitle)
        ///                 PlayButton(playState: $playState.updated { newState in
        ///                     model.playStateDidChange(newState)
        ///                 })
        ///             }
        ///         }
        ///     }
        ///
        /// - Parameters:
        ///   - action: A closure to run when the value changes.
        ///
        /// - Returns: A new binding value.
        func updated(_ action: @escaping (_ value: Value) -> Void) -> Binding<Value> {
            Binding(get: {
                wrappedValue
            }, set: { newValue in
                wrappedValue = newValue
                action(newValue)
            })
        }
    }
    

    There are two Binding extensions to use from iOS 13, watchOS 6 and macOS 10. The first .onUpdate() fires when the binding value changes, but does not give you access to the old or new values. It is JUST for side effects. I used this one above simply because the print() did not need any other value.

    If you need to use the newValue in your closure, use .updated. It works very similarly to .onChange(of:) except it modifies the Binding and does not give you access to the old value.