Backporting onChange(of:old:new:) for iOS 16


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 onChangeCompat and 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.