'onChange(of:perform:)' was deprecated in iOS 17.0となったので書き方を比較してみた

<!-- 記述ルールは画面下部にあります。(コメントは削除して問題ありません) -->

# 概要

今まで書いていた`onChange`が'onChange(of:perform:)' was deprecated in iOS 17.0となったので、公式ドキュメントを読んで解説を書きました。

## 環境
Xcode 15.0.1
以下の画像のようにプロジェクトのMinimum DeploymentのiOSが17.0以上であること

![Group 10568.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3576326/5b960c42-2839-a2f5-32fc-d1b6dd6ab57e.png)

- - -
# 詳細

iOS17.0より前のバージョンでは`onChange`の元の書き方は以下のように記載されていました。
[onChange(of:perform:)](https://developer.apple.com/documentation/swiftui/view/onchange(of:perform:)?ref=optimistic-closures.com)
```Swift:onChange:deprecated
struct MyScene: Scene {
    @Environment(\.scenePhase) private var scenePhase
    @StateObject private var cache = DataCache()


    var body: some Scene {
        WindowGroup {
            MyRootView()
        }
        .onChange(of: scenePhase) { newScenePhase in
            if newScenePhase == .background {
                cache.empty()
            }
        }
    }
}
```

`Environment`値や`Binding`値が更新されたタイミング(上記の例の場合は`scenePhase`が更新されたタイミング)でクロージャ内で定義した処理が動くという内容でした。

しかし、iOS17.0への移行に伴い、上記の書き方は**deprecated**となってしまいました。
Xcode上では以下のように警告が表示されるようになっているはずです。

:::note warn
'onChange(of:perform:)' was deprecated in iOS 17.0: Use `onChange` with a two or zero parameter action closure instead.
:::




iOS17.0への移行に伴い、`onChange`の新しい記法が２パターンほどAppleの公式ドキュメントに追加されており、
- `onChange`のクロージャ内で受け取る引数が0個,または2個となっている点
- `onChange`の受け取る引数に`initial:Bool`が追加された点
が以前と異なります。
それぞれの変更後の内容について以下で解説していきます。

## クロージャの引数が0個のonChange(of:initial:_:)

クロージャの引数が0個の`onChange`は今まで使用していた`onChange`と同じような使い方ができます。
以下がAppleの公式ドキュメントに記載されているサンプルコードです。

[onChange(of:initial:_:)](https://developer.apple.com/documentation/swiftui/view/onchange(of:initial:_:)-8wgw9?ref=optimistic-closures.com)

```Swift:PlayerView
/*
func onChange<V>(
    of value: V,
    initial: Bool = false,
    _ action: @escaping () -> Void
) -> some View where V : Equatable
*/


struct PlayerView: View {
    var episode: Episode
    @State private var playState: PlayState = .paused


    var body: some View {
        VStack {
            Text(episode.title)
            Text(episode.showTitle)
            PlayButton(playState: $playState)
        }
        .onChange(of: playState) {
            model.playStateDidChange(state: playState)
        }
    }
}
```

上記のサンプルコードのように、クロージャの引数が0個の場合、今まで書いていたクロージャの引数を明示的に書く必要がなくなりました。


**Depricated**の`onChange`を新しい`onChange`の記法で書き直した例が以下のようになります。

```Swift:PlayerView
// 古い書き方
.onChange(of: scenePhase) { newScenePhase in 
            if newScenePhase == .background {
                cache.empty()
            }
        }

// 新しい書き方
.onChange(of: scenePhase) { 
            if scenePhase == .background {
                cache.empty()
            }
        }
```

こう見ると、今までよりも記述量が減ってシンプルになった印象です。


## クロージャの引数が2個のonChange(of:initial:_:)

クロージャの引数が2個の`onChange`の場合、`of`内で追跡している値が変更された時に、追跡している値の**変更される前の値**と**変更された後の値**の両方を使用したい時に使用できます。

以下がAppleの公式ドキュメントに記載されているサンプルコードです。

[onChange(of:initial:_:)](https://developer.apple.com/documentation/swiftui/view/onchange(of:initial:_:)-4psgg?ref=optimistic-closures.com)


```Swift:PlayerView
/*
func onChange<V>(
    of value: V,
    initial: Bool = false,
    _ action: @escaping (V, V) -> Void
) -> some View where V : Equatable
*/


struct PlayerView: View {
    var episode: Episode
    @State private var playState: PlayState = .paused


    var body: some View {
        VStack {
            Text(episode.title)
            Text(episode.showTitle)
            PlayButton(playState: $playState)
        }
        .onChange(of: playState) { oldState, newState in
            model.playStateDidChange(from: oldState, to: newState)
        }
    }
}
```

こちらは変更される前の値から変更された後の値を引数として受け取る関数を使う場合や、それぞれの値を使用して比較を行う場合に使えます。
元々このような変更前の値を受け取ることはできたのですが、より簡潔に記述できるようになったという印象です。

こちらも古い書き方と新しい書き方を並べてみました。

```swift:PlayerView
// 古い書き方
struct PlayerView: View {
    var episode: Episode
    @State private var playState: PlayState = .paused


    var body: some View {
        VStack {
            Text(episode.title)
            Text(episode.showTitle)
            PlayButton(playState: $playState)
        }
        .onChange(of: playState) { [playState] newValue in
            model.playStateDidChange(from: playState, to: newValue)
        }
    }
}

// 新しい書き方
struct PlayerView: View {
    var episode: Episode
    @State private var playState: PlayState = .paused


    var body: some View {
        VStack {
            Text(episode.title)
            Text(episode.showTitle)
            PlayButton(playState: $playState)
        }
        .onChange(of: playState) { oldState, newState in
            model.playStateDidChange(from: oldState, to: newState)
        }
    }
}
```

以前は更新される前の値を使用するには`of:`で定義した値をそのままクロージャ内で宣言しなければならなかったので、コード自体の読みやすさが増したと思います。

## `onChange`の受け取る引数に`initial:Bool`が追加された点

上記のような記法の変更に加え、サンプルコードには記載されていませんが、引数として`initial:Bool`が追加されています。
この引数は初回起動時に`onChange`が動作するかを制御する引数で、
- `false`で設定した場合にはViewの初回表示時には`onChange`が動作しない
- `true`で設定した場合には初回表示時に`onChange`が動作する
といった設定ができます。
宣言時に引数を渡さなければデフォルト値の`false`が自動的に渡されます。

例えば、サンプルコードを以下のように修正すれば`PlayerView`の初回表示時に`onChange`が動作するようになります。

```swift:PlayerView
// 新しい書き方
struct PlayerView: View {
    var episode: Episode
    @State private var playState: PlayState = .paused


    var body: some View {
        VStack {
            Text(episode.title)
            Text(episode.showTitle)
            PlayButton(playState: $playState)
        }
        .onChange(of: playState,initial: true) {
            model.playStateDidChange(state: playState)
        }
    }
}

struct PlayerView: View {
    var episode: Episode
    @State private var playState: PlayState = .paused


    var body: some View {
        VStack {
            Text(episode.title)
            Text(episode.showTitle)
            PlayButton(playState: $playState)
        }
        .onChange(of: playState,initial: true) { oldState, newState in
            model.playStateDidChange(from: oldState, to: newState)
        }
    }
}
```