Open tsonobe1 opened 2 years ago
参考:https://swiftui-lab.com/state-changes/
Viewの中からViewのStateを更新したいときがある。 Xcodeはランタイム中に文句を言うため、DispatchQueueクロージャーの中に更新処理を置くだろう。 このやり方は、時にCPUスパイクやappクラッシュを起こすことがある。どうすればよいだろうか。
ViewのStateとは、Viewの全ての@Stateプロパティの事をさす。
Xcodeが次のランタイムメッセージを吐くことがある。
Modifying state during view update, this will cause undefined behavior.
ビューの更新中に状態を変更すると、未定義の動作が発生します
このメッセージが出たら、我々はDispatchQueue.main.async { } の中でStateを更新すればうまくいくことを知り、実行するだろう。 この方法を実行するときは、我々は「代替手段はないのか?」とモヤモヤするだろう。代替手段がある場合もある。ない場合もある。 このモヤモヤは、DispatchQueue.main.async { } テクニックが何を起こしているのかを知ることで晴れるだろう。
SwiftUIがViewのbodyを計算している最中に、Stateを変更するのはよくないことだ。 しかし、我々はよくViewのbodyの内部で.toggle()などを用いてブール値を更新したりしているではないか。 これはよいのか?
これは問題ない。 なぜなら、Stateの更新は、クロージャの内部で行われているから。 つまり、Stateの更新はViewのbody内に定義されているけど、実際の更新処理はtoggle()が発火した時だけ実行されるのだ。 だから、クロージャー内に定義されたViewのStateをViewのbody計算後に実行するのであれば、更新処理は完全に安心で、xcodeは文句を言わない。
このように、ランタイムエラーを出さずにViweの状態を更新できる場所は、 クロージャー内、onAppear, onDisappear, onPreferenceChange, onEndedなどになる
例えば次のように、Viewが計算されるたびにcounterを更新する例がある。
struct OutOfControlView: View { @State private var counter = 0 var body: some View { self.counter += 1 return Text("Computed Times\n\(counter)").multilineTextAlignment(.center) } }
これは「Modifying state during view update, this will cause undefined behavior.」を吐く そのため、はDispatchQueue.main.async { } の中に入れる
DispatchQueue.main.async { self.counter += 1 }
こうするとランタイムメッセージは消えるが、CPUがノンストップで増加しはじめる。 何が起こっているのだろうか。
Viewのbody計算後に、asyncクロージャーの中でStateを更新する。 するとViewが無効化し、改めてViewのbodyを計算する。 これを永遠と繰り返す
Stateに異なる値を設定しなくてよい場合を、SwiftUIに教えてあげれば良い。 Stateが本当に変化したときだけViewのbodyの再計算をすればよいことを教えてあげる
この例では、方位が変わったときだけ、新しいbodyを要求している https://swiftui-lab.com/state-changes/
ボタンを押したときだけ、要素が増えたときだけなど、特定の動作が起きたときだけ 関数(GeometryReaderが含まれたDispatchQueue.main.async)を実行するようにすれば、 無限ループは起きないということになる。
asyncクロージャを使用してViewのStateを更新するときは、 Stateの更新によるViewの再描画により、Stateが更新される仕組みになっていると無限ループが起きてしまう。 Stateの更新が「Viewの再描画」ではなく、それ以外の特定のきっかけになってることを確認すべき。
似たようなアンサー https://stackoverflow.com/a/67855586 https://github.com/renefx/swiftui_learn/blob/main/SwiftUI%20Learn/Drag%20and%20drop/Game/DragObjectView.swift
参考:https://swiftui-lab.com/state-changes/
話のテーマ
Viewの中からViewのStateを更新したいときがある。 Xcodeはランタイム中に文句を言うため、DispatchQueueクロージャーの中に更新処理を置くだろう。 このやり方は、時にCPUスパイクやappクラッシュを起こすことがある。どうすればよいだろうか。
定義
ViewのStateとは、Viewの全ての@Stateプロパティの事をさす。
エラーメッセージの意味
Xcodeが次のランタイムメッセージを吐くことがある。
Modifying state during view update, this will cause undefined behavior.
ビューの更新中に状態を変更すると、未定義の動作が発生します
これは、「View本体の計算中にStateが更新されるため、新しいViewの計算が次々に発生しているよ。これは処理できないので、あなたが期待するStateは未定義にしますよ!何もしませんよ!」という意味。解決策とモヤモヤ
このメッセージが出たら、我々はDispatchQueue.main.async { } の中でStateを更新すればうまくいくことを知り、実行するだろう。 この方法を実行するときは、我々は「代替手段はないのか?」とモヤモヤするだろう。代替手段がある場合もある。ない場合もある。 このモヤモヤは、DispatchQueue.main.async { } テクニックが何を起こしているのかを知ることで晴れるだろう。
View内部でStateの値を更新するのってマズいの?
SwiftUIがViewのbodyを計算している最中に、Stateを変更するのはよくないことだ。 しかし、我々はよくViewのbodyの内部で.toggle()などを用いてブール値を更新したりしているではないか。 これはよいのか?
これは問題ない。 なぜなら、Stateの更新は、クロージャの内部で行われているから。 つまり、Stateの更新はViewのbody内に定義されているけど、実際の更新処理はtoggle()が発火した時だけ実行されるのだ。 だから、クロージャー内に定義されたViewのStateをViewのbody計算後に実行するのであれば、更新処理は完全に安心で、xcodeは文句を言わない。
このように、ランタイムエラーを出さずにViweの状態を更新できる場所は、 クロージャー内、onAppear, onDisappear, onPreferenceChange, onEndedなどになる
View内部でStateの値を更新したらマズい例
例えば次のように、Viewが計算されるたびにcounterを更新する例がある。
これは「Modifying state during view update, this will cause undefined behavior.」を吐く そのため、はDispatchQueue.main.async { } の中に入れる
こうするとランタイムメッセージは消えるが、CPUがノンストップで増加しはじめる。 何が起こっているのだろうか。
Viewのbody計算後に、asyncクロージャーの中でStateを更新する。 するとViewが無効化し、改めてViewのbodyを計算する。 これを永遠と繰り返す
DispatchQueue.main.asyncの中でStateを更新すべき時
Stateに異なる値を設定しなくてよい場合を、SwiftUIに教えてあげれば良い。 Stateが本当に変化したときだけViewのbodyの再計算をすればよいことを教えてあげる
この例では、方位が変わったときだけ、新しいbodyを要求している https://swiftui-lab.com/state-changes/
ボタンを押したときだけ、要素が増えたときだけなど、特定の動作が起きたときだけ 関数(GeometryReaderが含まれたDispatchQueue.main.async)を実行するようにすれば、 無限ループは起きないということになる。
まとめ
asyncクロージャを使用してViewのStateを更新するときは、 Stateの更新によるViewの再描画により、Stateが更新される仕組みになっていると無限ループが起きてしまう。 Stateの更新が「Viewの再描画」ではなく、それ以外の特定のきっかけになってることを確認すべき。