Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I run an action when a state changes?

People also ask

How does onChange work SwiftUI?

SwiftUI lets us attach an onChange() modifier to any view, which will run code of our choosing when some state changes in our program. This is important, because we can't always use property observers like didSet with something like @State .

What is state SwiftUI?

SwiftUI manages the storage of a property that you declare as state. When the value changes, SwiftUI updates the parts of the view hierarchy that depend on the value. Use state as the single source of truth for a given value stored in a view hierarchy.


You can't use didSet observer on @State but you can on an ObservableObject property.

import SwiftUI
import Combine

final class SelectionStore: ObservableObject {
    var selection: SectionType = .top {
        didSet {
            print("Selection changed to \(selection)")
        }
    }

    // @Published var items = ["Jane Doe", "John Doe", "Bob"]
}

Then use it like this:

import SwiftUI

enum SectionType: String, CaseIterable {
    case top = "Top"
    case best = "Best"
}

struct ContentView : View {
    @ObservedObject var store = SelectionStore()

    var body: some View {
        List {
            Picker("Selection", selection: $store.selection) {
                ForEach(FeedType.allCases, id: \.self) { type in
                    Text(type.rawValue).tag(type)
                }
            }.pickerStyle(SegmentedPickerStyle())

            // ForEach(store.items) { item in
            //     Text(item)
            // }
        }
    }
}

iOS 14.0+

You can use the onChange(of:perform:) modifier, like so:

struct ContentView: View {
    
    @State private var isLightOn = false

    var body: some View {
        Toggle("Light", isOn: $isLightOn)
            .onChange(of: isLightOn) { value in
                if value {
                    print("Light is now on!")
                } else {
                    print("Light is now off.")
                }
            }
    }
}

iOS 13.0+

The following as an extension of Binding, so you can execute a closure whenever the value changes.

extension Binding {
    
    /// When the `Binding`'s `wrappedValue` changes, the given closure is executed.
    /// - Parameter closure: Chunk of code to execute whenever the value changes.
    /// - Returns: New `Binding`.
    func onUpdate(_ closure: @escaping () -> Void) -> Binding<Value> {
        Binding(get: {
            wrappedValue
        }, set: { newValue in
            wrappedValue = newValue
            closure()
        })
    }
}

Used like so for example:

struct ContentView: View {
    
    @State private var isLightOn = false
    
    var body: some View {
        Toggle("Light", isOn: $isLightOn.onUpdate(printInfo))
    }
    
    private func printInfo() {
        if isLightOn {
            print("Light is now on!")
        } else {
            print("Light is now off.")
        }
    }
}

This example doesn't need to use a separate function. You only need a closure.


Here is another option if you have a component that updates a @Binding. Rather than doing this:

Component(selectedValue: self.$item, ...)

you can do this and have a little greater control:

Component(selectedValue: Binding(
    get: { self.item },
    set: { (newValue) in
              self.item = newValue
              // now do whatever you need to do once this has changed
    }), ... )

This way you get the benefits of the binding along with the detection of when the Component has changed the value.


SwiftUI 1 & 2 (iOS 13 & 14)

You can use onReceive:

import Combine
import SwiftUI

struct ContentView: View {
    @State private var selection = false

    var body: some View {
        Toggle("Selection", isOn: $selection)
            .onReceive(Just(selection)) { selection in
                // print(selection)
            }
    }
}

In iOS 14 there is now a onChange modifier you can use like so:

SegmentedControl(selection: $selection) {
    ForEach(SectionType.allCases.identified(by: \.self)) { type in
        Text(type.rawValue).tag(type)
    }
}
.onChange(of: selection) { value in
    print("Selection changed to \(selection)")
}

You can use Binding

let textBinding = Binding<String>(
    get: { /* get */ },
    set: { /* set $0 */ }
)