Performance (SwiftUI)
基本ルール
View の initializer (初期化) は低コストにする
デフォルト引数を持ったプロパティがいる場合、そのデフォルト引数の生成が高コストな場合も注意する
View の body は副作用のない純粋な関数にする
View の initializer や body がどのくらい呼び出されるかを仮定しない
動的プロパティのインスタンスが高コストなパターン
View body が高コストなパターン
高コストな文字列の補完
頻繁に使う文字列はキャッシュしておく
バンドルからの値の検索
データ・フィルタリング
余分な再描画が行われる
綺麗なアニメーションが行われない
状態が消失する可能性がある
UUID のような安定した ID を用いて、名前のような重複/変化しやすい値は使用しない
更新プロセス
code:swift
struct CatView: View {
@Environment(\.isPlayTime) private var isPlayTime
var cat: Cat
var body: some View {
CatImage(cat)
CatDetail(cat)
}
}
上記のようなViewがあった時、データモデルが変更されると、以下のような手順でViewが更新される。
1. CatView の新しい値が作られる
cat の値が初期化される
他にも動的プロパティの初期値などがあれば、このタイミングで初期化される
3. 更新された値で View を生成するために body が実行される
更新対象
code:swift
struct CatImage: View {
var cat: Cat
var body: some View {
cat.image
.resizable()
.frame(width: 200, height: 200)
.padding()
}
}
が、この View が必要なのは cat.image だけなので、本来であれば再描画の必要はないかもしれない。そこで、依存を以下のように最低限にする。
code:swift
struct CatImage: View {
var catImage: Image
var body: some View {
catImage
.resizable()
.frame(width: 200, height: 200)
.padding()
}
}
これにより、不要な子 View の再描画を避けることができる。
Self._printChanges を利用すると、View が更新された理由をチェックすることができる。LLDB から実行したり、View の body に仕込んだりする。ただし、仕込んだままリリースしないように注意すること。 Listの最適化
以下のような単純なリストを考えてみる。
code:swift
List {
ForEach(cats) {
CatCell(cat: $0)
}
}
ForEach はデータのコレクションを View にマップし、各 View のために explicit identity を生成する。List はその描画のために事前に表示する行数を把握しておく必要があるので、ForEach の保持するデータのコレクションにアクセスし、各要素の Identity (SwiftUI) を参照し、行数を数える。一方、行に対応する View は、それが描画されるタイミングでのみ動的に生成されるため、List の事前処理は基本的に低コストになる。 ただし、ForEach が条件を満たさない場合には、List はその行数の確定のために List 内の全ての行の View を事前に描画する必要がある。この条件とは、データと生成されるViewの数が1:1であることである。
以下に例を示す。
code:swift
List {
ForEach(cats) { cat in
// ❌ データコンテンツに対するViewが1か0で可変のためViewを全て描画しないと行数が確定しない
if cat.isWhite {
CatCell(cat)
}
}
}
List {
ForEach(cats) { cat in
// ❌ データコンテンツに対するViewの数が不明のため、Viewを全て描画しないと行数が確定しない
AnyView(...)
}
}
ForEach 内で View 構造をいじるのではなく、事前にモデルを変換するのが良い。ただし、View の body 内ではなく、そもそも変換後のモデルを body に与えるのが効率が良い。
code:swift
List {
// 🔺 フィルタ処理が線形なので、データ量が増えるに従って重くなる
ForEach(cats.filter(...)) { cat in
CatCell(cat)
}
}
// ⭕️
List {
ForEach(whiteCats) { cat in
CatCell(cat)
}
}
List のポイントとしては、
ForEach をなるだけネストしないこと
セクション化されている場合はOK (SwiftUI は Section の構造を理解して効率的に動作する) Listの行数を、「要素数*要素毎のView数」になるように保つのが大事
Tableの最適化
Listの最適化とほぼ同じ