WWDC Quick Look 💓 By SwiftGGTeam
Demystify SwiftUI

Demystify SwiftUI

元の動画を見る

ハイライト

SwiftUI のレンダリング動作は、3 つの中心的な概念によって決定されます。ID はビューが再利用されるかどうかを決定し、Lifetime は状態がいつリセットされるかを決定し、Dependency はいつ更新されるかを決定します。これら 3 つを理解すれば、SwiftUI の奇妙なバグのほとんどを取り除くことができます。

主な内容

SwiftUI を作成するとき、開発者はよく 3 種類の問題に遭遇します。ステータスが不可解にリセットされる、アニメーションがトリガーされない、ビューが頻繁に更新されすぎるということです。これらの問題は、表面的にはバグのように見えますが、根本的な原因は、多くの場合、SwiftUI のコアメカニズムの理解が不十分であることです。

このセッションでは新しい API については説明しませんが、SwiftUI の根底にある哲学についての詳細な分析を提供します。

アイデンティティ: SwiftUI がビューを識別する方法

Identity は、「このビューとあのビューは 2 つのレンダリングで同じですか?」という質問に答えます。

SwiftUI は 2 つの ID を使用します。

構造的同一性: ビュー ツリー内のビューの位置によって決定されます。同じ位置は同じビューを意味します。

明示的なアイデンティティ: パス.id()修飾子が指定されました。ID が変更されると、SwiftUI はそれが完全に新しいビューであると認識します。

ForEach(items) { item in
    RowView(item: item)
}

ここitemIDはそれぞれですRowView身元。あなたが置くならitems並べ替える場合、SwiftUI は既存のビュー インスタンスを再利用し、データのみを更新します。

しかし、次のように書かれているとします。

ForEach(0..<items.count, id: \.self) { index in
    RowView(item: items[index])
}

要素が配列の中央に挿入されると、後続のすべての要素のインデックスと ID が変更されます。 SwiftUI はこれらのビューを破棄して再構築し、すべての状態が失われます。

ライフタイム: 状態のライフサイクル

@Stateそして@StateObjectライフサイクルはビューの ID にバインドされています。アイデンティティが変わらなければ、状態は保持されます。 ID が変更されると、状態はリセットされます。

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        Button("Count: \(count)") {
            count += 1
        }
        .id(someValue) // 当 someValue 变化,count 重置为 0
    }
}

依存関係: SwiftUI がいつ更新するかを知る方法

SwiftUI のbodyすべての読み取り依存関係は、実行中に自動的に追跡されます。依存関係が変更されると、ビューが自動的に更新されます。

struct UserView: View {
    @ObservedObject var user: User

    var body: some View {
        Text(user.name) // SwiftUI 记住了:这个视图依赖 user.name
    }
}

もしuser.name変更すると、SwiftUI がこのビューを更新します。しかし、もしuser.ageこのビューでは読み込まずに変更されますuser.age, SwiftUIでは更新されません。

##詳細

よくあるアイデンティティの罠

条件分岐切り替えビュータイプ

if isEditing {
    TextField("Name", text: $name)
} else {
    Text(name)
}

いつisEditing切り替えると、TextField と Text はビュー ツリー内の同じ位置を占めますが、タイプは異なります。SwiftUI は TextField を破棄して Text を作成し、次に切り替えるときに Text を破棄して新しい TextField を作成します。TextField のフォーカス状態が失われます。

修正: 安定した ID を両方のビューに追加します。

if isEditing {
    TextField("Name", text: $name)
        .id("nameField")
} else {
    Text(name)
        .id("nameField")
}

このようにして、SwiftUI は、タイプが異なるだけで、それらが同じ論理ビューであることを認識します。

ForEach は不安定な ID を使用します

ForEach(items.indices, id: \.self) { index in
    // index 作为 id,插入元素后全部重建
}

修正: データ自体の安定した識別を使用します。

ForEach(items, id: \.id) { item in
    // item.id 是稳定的
}

@StateObject vs @ObservedObject

struct ParentView: View {
    @StateObject private var model = ViewModel()

    var body: some View {
        ChildView(model: model)
    }
}

struct ChildView: View {
    @ObservedObject var model: ViewModel

    var body: some View {
        Text(model.title)
    }
}

キーポイント:

  • @StateObjectビューが最初に作成されたときに初期化し、後で同じインスタンスを再利用します。
  • @ObservedObject外部から渡されたオブジェクトを観察するだけで、そのライフサイクルを管理しない
  • 決して入ってはいけませんbody作成年ObservableObjectインスタンスは、更新するたびに新しいインスタンスが作成されます

依存関係の正確な追跡

SwiftUI の依存関係の追跡はプロパティ レベルで行われます。

struct DetailView: View {
    let item: Item

    var body: some View {
        VStack {
            Text(item.title)
            Text(item.description)
        }
    }
}

もしitem親ビューが新しい値を渡すたびに、値型 (構造体) になります。item(内容が同じでも)SwiftUIが更新されますDetailView

もしitem参照型(クラス)であり、SwiftUIは参照アドレスを比較します。別のインスタンスが渡された場合にのみ更新されます。

重要ポイント

1.安定したアイデンティティを使用してリストを設計します

編集可能なリストを実装する場合は、各セルの ID が配列インデックスではなくデータのビジネス ID に基づいていることを確認してください。このようにして、SwiftUI は挿入、削除、並べ替えの際にビューを正しく再利用できるようになり、アニメーションがよりスムーズになります。入口API:ForEach(items, id: \.id)

2. ViewModel のライフサイクルをビューから切り離します

使用@StateObjectルート ビューで ViewModel を作成します。@ObservedObjectまたはサブビューに渡される環境オブジェクト。このようにして、ID の変更によりサブビューが再構築された場合でも、データは失われません。入口API:@StateObject + environmentObject()

3. EquatableView を使用して大量の静的コンテンツを最適化します

めったに変更されない多数のサブビューを含むコンテナの場合は、次のように実装します。Equatableプロトコルとしてパッケージ化されていますEquatableView, SwiftUI が不要な diff をスキップできるようにします。入口API:static func == + .equatable()

4.状態リセットの問題の診断

State が予期せずリセットされたことに気付いた場合は、次の 3 つの点を確認してください。.id()そして、ForEach の ID が安定しているかどうか、条件分岐内のビューが同じ位置を共有しているかどうかなど、値が変更されました。Xcode の SwiftUI デバッグ ツールを使用して、ビュー ツリーで ID の変更を表示します。

5.予測可能なリフレッシュ境界を設計する

ページ全体の更新がトリガーされるのを避けるために、頻繁に変更されるデータ (タイマー、センサー データなど) を独立したサブビューに分離します。エントランスAPI:動的部分を独立したViewに抽出し、必要なデータのみを渡します。

関連セッション

コメント

GitHub Issues · utterances