Starting with iOS 17, SwiftUI introduced a new overload of the onChange(of:)
modifier. This version includes both the old and new values in the closure, making many state-driven UI updates cleaner and more reliable.
However, when targeting iOS 16 and earlier, this version of onChange
is unavailable. To provide consistent behavior across OS versions, it’s useful to implement a backported version that mimics the same API surface.
This article describes a small extension that adds compatibility support for the two-value onChange
modifier and a utility based on it.
Compatibility Extension for onChange
The following View
extension introduces onChangeCompat(of:_:)
. It accepts a value conforming to Equatable
and a closure that receives both the previous and new value.
import SwiftUI
public extension View {
@available(iOS, deprecated: 17.0, message: "Use `onChange(of:initial:_:)` instead.")
@ViewBuilder
func onChangeCompat<V>(
for value: V,
_ action: @escaping (_ previous: V, _ current: V) -> Void
) -> some View where V: Equatable {
if #available(iOS 17, *) {
onChange(of: value, action)
} else {
onChange(of: value) { [value] newValue in
action(value, newValue)
}
}
}
}
On iOS 17 and later, it delegates directly to the official overload. On earlier versions, it simulates the oldValue
by capturing the current value at the time of render.
Detecting Transitions from a Specific Value
Building on top of the backported onChange
, the onTransition
helper allows observing a specific type of transition: when a value changes from one known state to something else.
public extension View {
func onTransition<V>(
of value: V,
from previousValue: V,
perform action: @escaping () -> Void
) -> some View where V: Equatable {
onChangeCompat(for: value) { old, new in
if old == previousValue && new != previousValue {
action()
}
}
}
}
This pattern is useful when responding to a change away from a certain value, such as dismissing a view, toggling focus, or tracking selection state transitions.
Summary
The combination of
and onChangeCompat
onTransition
allows developers to unify the behavior of change-tracking logic across iOS 16 and 17, avoiding the need for conditional compilation or custom view logic. This approach can be applied anywhere Equatable
state drives SwiftUI views and interactions.
If you enjoyed this article, please feel free to follow me on my social media: