| < How to use a timer with SwiftUI | How to show different images and other views in light or dark mode > |
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.
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.
Sponsor Hacking with Swift and reach the world's largest Swift community!
Link copied to your pasteboard.