バッテラのハローワールド研究室

エンジニア、プログラミングに関する情報を掲載中!

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

SwiftUI

はじめに

今日はSwiftUIを勉強していきます。

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

憶測で言ってるので間違っているかもしれません。ご了承くださいませ。

環境

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

* MacOS Monterey (12.1)
* Xcode (13.3.1)

症状

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

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

追加は正常に更新されてる

原因

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

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

struct RowView: View {
    @State var title: String

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

んー。なんでだろ。

調査

この問題にあたり調査方法として_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は表示数が必然的に多くなるため負荷を下げるために使い回してる構造になってるかもしれないです。(憶測です)

対策

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が同じだと再利用できる場合はするといった挙動をしている気がします。

この方法は最終手段で使いましょう。

参考にしたサイト

blog.ch3cooh.jp

www.yururiwork.net

teratail.com

おわりに

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

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