バッテラが如く

プログラミングしましょ!

【SwiftUI】Listでセル(子View)が再描画されない件について

この記事の情報は次のバージョンで動作確認しています。

  • MacOS Monterey (12.1)
  • Xcode (13.3.1)
  • SwiftUI

    はじめに

    SwiftUIのListを使っていく中で起きた問題についてになります。

    ForEachで作ったリストアイテムが変更してるのに更新されない問題が起きたので共有します。

    私のSwifUIの知識が浅いので使い方の問題なのかしれないのですが、同じ問題が起きてる方はこれを見ていただければ解決できると思います。

    事象について

    挙動

    ・配列の要素を更新してもアイテムが変更されていない。

    ↓の動画では詳細画面でABCという名称にしたのにトップ画面だとAのままになっています

    ・配列の要素追加は正常で動作する

    ここでわかるのは配列の*要素の変更を検知できていないという点です。

    ダメなプログラム例

    @State変数をリストの子Viewで使っており、親Viewがイニシャライザで渡すという作りをしてました。

    親Viewの再描画が走ったらイニシャライザから新しい値が渡り子Viewが更新されると思っていました。

    struct RowView: View {
        @State var title: String
    

    RowViewinitprintでログ出す分には更新されてるけども、表示されるのは変更前の状態となります。

    つまり変数は変更済みだが見た目が変更前になってしまってるということです。

    んー。なんでだろ。

    解決方法

    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が変化したかをログで追うことができます。

    www.m2game.net

    再描画されないViewのbodyで_printChangesを仕込んでアプリを起動しましょう。

    メンバ変数名が _title, _description changed.といった感じで羅列されると思います。

    初回表示時は @self, @identity と 自分で宣言した変数(@がつくものとつかないもの両方) が changed.と出ます。

    見て欲しいのは再描画時です。

    該当のViewが再描画される場合は必ずログが表示されます。

    ログに表示されていない変数名に注目しましょう。(@self, @identityは除いていいです)

    こうしてやってみると通常の変数はchanged.が表示されると思います。

    しかし@Stateの変数がログに表示されない時があります。変数の中身自体をprintで表示したら新しい値(正常値)になっています。

    この時実際の画面で表示されるのは前回表示されていた値、つまり更新されていない状態でした。

    こんな感じで調査していきました。

    List以外はどうなのか?

    NavigationLink.sheetでViewを表示すると、毎回全部の変数名(@Stateがつくやつも)が changed.と表示されています。

    画面遷移で表示するViewは完全新規で作られる挙動だと思います。

    Listは表示数が必然的に多くなるため負荷を下げるために使い回してる構造になってるかもしれないです。(憶測です)

    参考にしたサイト

    blog.ch3cooh.jp

    www.yururiwork.net

    teratail.com

    おわりに

    最後まで見ていただきヘペトナス!

    読者登録・Twitterのフォローもお願いします。