TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

How to run some code when state changes using onChange()

Paul Hudson    @twostraws   

Updated for Xcode 16

Improved in iOS 17

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.

Important: This behavior is changing in iOS 17 and later, with the older behavior being deprecated.

If you need to target iOS 16 and earlier, onChange() accepts one parameter and sends its new value into a closure of your choosing. For example, this will print name changes as they are typed:

struct ContentView: View {
    @State private var name = ""

    var body: some View {
        TextField("Enter your name:", text: $name)
            .textFieldStyle(.roundedBorder)
            .onChange(of: name) { newValue in
                print("Name changed to \(name)!")
            }
    }
}

Download this as an Xcode project

If you’re targeting iOS 17 or later, there’s a variant that accepts no parameters – you can just read the property directly and be sure to get its new value, which isn’t how the single-parameter version worked in iOS 16 and earlier.

iOS 17 also provides two other variants: one that accepts a two closure with parameters, one for the old value and one for the new value, and one that determines whether your action function should be run when your view is first shown.

For example, this prints out both the old and new value when a change happens:

struct ContentView: View {
    @State private var name = ""

    var body: some View {
        TextField("Enter your name", text: $name)
            .onChange(of: name) { oldValue, newValue in
                print("Changing from \(oldValue) to \(newValue)")
            }
    }
}

Download this as an Xcode project

And this prints a simple message when the value changes, but by adding initial: true also triggers the action closure when the view is shown:

struct ContentView: View {
    @State private var name = ""

    var body: some View {
        TextField("Enter your name", text: $name)
            .onChange(of: name, initial: true) {
                print("Name is now \(name)")
            }
    }
}

Download this as an Xcode project

Using initial: true is a really helpful way to consolidate functionality – rather than having to do some work in onAppear() and onChange(), you can do it all in one pass.

You might prefer to add a custom extension to Binding so that I attach observing code directly to the binding rather than to the view – it lets me place the observer next to the thing it’s observing, rather than having lots of onChange() modifiers attached elsewhere in my view.

That would mean using code like this:

extension Binding {
    func onChange(_ handler: @escaping (Value) -> Void) -> Binding<Value> {
        Binding(
            get: { self.wrappedValue },
            set: { newValue in
                self.wrappedValue = newValue
                handler(newValue)
            }
        )
    }
}

struct ContentView: View {
    @State private var name = ""

    var body: some View {
        TextField("Enter your name:", text: $name.onChange(nameChanged))
            .textFieldStyle(.roundedBorder)
    }

    func nameChanged(to value: String) {
        print("Name changed to \(name)!")
    }
}

Download this as an Xcode project

That being said, please be sure to run your code through Instruments if you do this – using onChange() on a view is more performant than adding it to a binding.

Hacking with Swift is sponsored by Superwall.

SPONSORED Superwall lets you build & test paywalls without shipping updates. Run experiments, offer sales, segment users, update locked features and more at the click of button. Best part? It's FREE for up to 250 conversions / mo and the Superwall team builds out 100% custom paywalls – free of charge.

Learn more

Sponsor Hacking with Swift and reach the world's largest Swift community!

Similar solutions…

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.4/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.