tsonobe1 / SwiftUIPlayGround

0 stars 0 forks source link

【訳】DispatchQueueでViewのStateを更新しても大丈夫な時と、そうでない時 #19

Open tsonobe1 opened 2 years ago

tsonobe1 commented 2 years ago

参考:https://swiftui-lab.com/state-changes/

話のテーマ

Viewの中からViewのStateを更新したいときがある。 Xcodeはランタイム中に文句を言うため、DispatchQueueクロージャーの中に更新処理を置くだろう。 このやり方は、時にCPUスパイクやappクラッシュを起こすことがある。どうすればよいだろうか。

定義

ViewのStateとは、Viewの全ての@Stateプロパティの事をさす。

エラーメッセージの意味

Xcodeが次のランタイムメッセージを吐くことがある。

解決策とモヤモヤ

このメッセージが出たら、我々は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を更新する例がある。

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を計算する。 これを永遠と繰り返す

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の再描画」ではなく、それ以外の特定のきっかけになってることを確認すべき。

tsonobe1 commented 2 years ago

似たようなアンサー https://stackoverflow.com/a/67855586 https://github.com/renefx/swiftui_learn/blob/main/SwiftUI%20Learn/Drag%20and%20drop/Game/DragObjectView.swift