概要
今まで書いていたonChange
が'onChange(of:perform:)' was deprecated in iOS 17.0となったので、公式ドキュメントを読んで解説を書きました。
環境
Xcode 15.0.1
以下の画像のようにプロジェクトのMinimum DeploymentのiOSが17.0以上であること
詳細
iOS17.0より前のバージョンではonChange
の元の書き方は以下のように記載されていました。
onChange(of:perform:)
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上では以下のように警告が表示されるようになっているはずです。
'onChange(of:perform:)' was deprecated in iOS 17.0: Use onChange
with a two or zero parameter action closure instead.
iOS17.0への移行に伴い、onChange
の新しい記法が2パターンほどAppleの公式ドキュメントに追加されており、
-
onChange
のクロージャ内で受け取る引数が0個,または2個となっている点 -
onChange
の受け取る引数にinitial:Bool
が追加された点
が以前と異なります。
それぞれの変更後の内容について以下で解説していきます。
クロージャの引数が0個のonChange(of:initial:_:)
クロージャの引数が0個のonChange
は今まで使用していたonChange
と同じような使い方ができます。
以下がAppleの公式ドキュメントに記載されているサンプルコードです。
/*
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
の記法で書き直した例が以下のようになります。
// 古い書き方
.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の公式ドキュメントに記載されているサンプルコードです。
/*
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)
}
}
}
こちらは変更される前の値から変更された後の値を引数として受け取る関数を使う場合や、それぞれの値を使用して比較を行う場合に使えます。
元々このような変更前の値を受け取ることはできたのですが、より簡潔に記述できるようになったという印象です。
こちらも古い書き方と新しい書き方を並べてみました。
// 古い書き方
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
が動作するようになります。
// 新しい書き方
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)
}
}
}