この記事の情報は次のバージョンで動作確認しています。
はじめに
SwiftUIのListを使っていく中で起きた問題についてになります。
ForEachで作ったリストアイテムが変更してるのに更新されない問題が起きたので共有します。
私のSwifUIの知識が浅いので使い方の問題なのかしれないのですが、同じ問題が起きてる方はこれを見ていただければ解決できると思います。
参考にした書籍
事象について
挙動
・配列の要素を更新してもアイテムが変更されていない。
↓の動画では詳細画面でABCという名称にしたのにトップ画面だとAのままになっています
・配列の要素追加は正常で動作する
ここでわかるのは配列の*要素の変更を検知できていないという点です。
ダメなプログラム例
@State
変数をリストの子Viewで使っており、親Viewがイニシャライザで渡すという作りをしてました。
親Viewの再描画が走ったらイニシャライザから新しい値が渡り子Viewが更新されると思っていました。
struct RowView: View { @State var title: String
RowView
のinit
でprint
でログ出す分には更新されてるけども、表示されるのは変更前の状態となります。
つまり変数は変更済みだが見た目が変更前になってしまってるということです。
んー。なんでだろ。
解決方法
Listの子Viewは値を渡すだけにする
@State
を使わないのもありますが、そもそもデータをそのまま表示させるという作りにすべきです。
プリミティブなデータ側を渡す分には確実に再描画されるので、まずはその作りにしませんか?
というのが答えだと思います。
ToggleやTextFieldは使いたい場合 (未解決)
これらのViewはイニシャライザでBinding型の引数がマストであり、@Stateを必要としてきます。。
非常にやっかいです。自分の中でのベストプラクティスがまだない状態です。
ToggleはButtonに置き換えれば@Stateは不要になりますが、TextFieldは難しそうですね。。
NavigationLinkで別画面に飛ばして変更させるとかになりそう。
どうしても解決できない場合 (パワープレイ)
Listにおけるデータがstruct(構造体)である場合は、idを変えれば更新されます。
更新処理をした後に該当の要素のid
を書き換えます
items[index].id = UUID() // idのデータ型に合わせてユニークな値で更新
なぜか分かりませんがid
を変えてログを見てみると全ての変数がchanged.
と表示されます。
これにより完全新規として扱われているため再描画が正しく表示されます。
よく分かりませんがListはid
が同じだと再利用できる場合はするといった挙動をしている気がします。
この方法は最終手段で使いましょう。
調査で分かったこと
この問題にあたり調査方法として_printChanges
を使いました。
これを使うことで@State
などのProperty Wrapper
が変化したかをログで追うことができます。
再描画されないViewのbodyで_printChanges
を仕込んでアプリを起動しましょう。
メンバ変数名が _title, _description changed.
といった感じで羅列されると思います。
初回表示時は @self, @identity
と 自分で宣言した変数(@がつくものとつかないもの両方) が changed.
と出ます。
見て欲しいのは再描画時です。
該当のViewが再描画される場合は必ずログが表示されます。
ログに表示されていない変数名に注目しましょう。(@self, @identity
は除いていいです)
こうしてやってみると通常の変数はchanged.
が表示されると思います。
しかし@Stateの変数がログに表示されない時があります。変数の中身自体をprintで表示したら新しい値(正常値)になっています。
この時実際の画面で表示されるのは前回表示されていた値、つまり更新されていない状態でした。
こんな感じで調査していきました。
List以外はどうなのか?
NavigationLink
や.sheet
でViewを表示すると、毎回全部の変数名(@Stateがつくやつも)が changed.
と表示されています。
画面遷移で表示するViewは完全新規で作られる挙動だと思います。
Listは表示数が必然的に多くなるため負荷を下げるために使い回してる構造になってるかもしれないです。(憶測です)
参考にしたサイト
おわりに
最後まで見ていただきヘペトナス!
読者登録・Twitterのフォローもお願いします。