In iOS 17, Apple deprecated the onChange(of:perform) view modifier replacing it with two new variations.
Pre-iOS 17 onChange
Before iOS 17, you use the onChange(of:perform:)
view modifier to run an action when a property of a view changes. For example, here’s a list view that scrolls to the last item each time the count of items in the list changes:
struct ListView: View {
@EnvironmentObject private var store: ItemStore
var count: Int {
store.itemCount
}
var body: some View {
ScrollViewReader { proxy in
List {
ForEach(store.items) { item in
NavigationLink(value: item) { ItemRow(item: item) }
}
}
.onChange(of: count) { newCount in
if let lastID = store.lastItemID() {
withAnimation {
proxy.scrollTo(lastID)
}
}
}
}
}
}
The closure has a single argument for the new value of the count. If I only want to scroll when the count increases I need to capture the value before the state changes:
.onChange(of: count) { [count] newCount in
if newCount > count,
let lastID = store.lastItemID {
withAnimation {
proxy.scrollTo(lastID)
}
}
}
Note: The closure captures the value before the change.
onChange in iOS 17
Once you update the minimum deployment target of your app to iOS 17 Xcode shows a deprecation warning for any use of the old onChange handler:
You have two choices depending on whether you want the value that’s changed or not. For situations where you don’t care about the value use the closure with zero arguments:
.onChange(of: count) {
if let lastID = store.lastItemID {
withAnimation {
proxy.scrollTo(lastID)
}
}
}
The alternate version gives you both the old and new values of the tracked value:
.onChange(of: count, initial: true) { oldCount, newCount in
if newCount > oldCount,
let lastID = store.lastItemID {
withAnimation {
proxy.scrollTo(lastID)
}
}
}
Initial Action
Both these new methods have a boolean initial property that defaults to false. When true the action runs when the view initially appears. For example, if I want to scroll to the end of the list when the view appears:
.onChange(of: count, initial: true) { oldCount, newCount in
if newCount >= oldCount,
let lastID = store.lastItemID {
withAnimation {
proxy.scrollTo(lastID)
}
}
}
Note that on the initial run the old and new values are the same.