WWDC Quick Look 💓 By SwiftGGTeam
Demystify SwiftUI performance

Demystify SwiftUI performance

元の動画を見る

ハイライト

iOS 17 および macOS Sonoma の SwiftUI により、開発者はSelf._printChanges()冗長な更新を見つけるには、次を使用します。@Observable依存関係の範囲を減らし、安定した ForEach コンテンツ構造を使用して完全な再構築を回避することで、インターフェイスの応答速度が体系的に向上します。

主な内容

依存関係が多すぎると不必要な更新が発生します

SwiftUI を作成するときに、無関係なプロパティが変更され、ビュー ツリー全体が再計算されるという状況に遭遇したことがあるはずです。たとえば、犬の詳細を表示するページでは、犬の名前のみを更新する必要がありますが、画像も更新されます。

03:59

SwiftUI は依存関係グラフを使用して、どのビューを更新する必要があるかを判断します。各ビューの本体は、入力値と動的プロパティに基づいて計算されます。いつDog構造が変更されると、それに依存するすべてのサブビューが再計算されます。問題は、一部のサブビューは実際には知る必要がないことですDogすべての情報。

04:05

Apple の解決策は、依存関係の範囲を狭めることです。全体を入れるのではなく、Dog渡されるオブジェクトScalableDogImage、必要なものだけを渡す方が良いですImage。このようにして、犬の他の属性が変更された場合、画像ビューは更新通知を受け取りません。

08:48

同期ブロッキングによりインターフェイスがスタックする

もう 1 つの一般的なシナリオは、ビューの初期化中に時間のかかる操作を実行することです。たとえば、あなたが次のような場合@State物体init()内部でネットワーク リクエストを開始します。このリクエストはメイン スレッドをブロックし、インターフェイスをフリーズさせます。

12:22

正しいアプローチは、時間のかかる操作を非同期タスクに移動し、.taskモディファイアは、ビューが表示されるときに実行されます。こうすることで、本体をすぐに返すことができ、インターフェイスの応答性が維持されます。

12:23

リストとテーブルのアイデンティティ トラップ

選択、スライド操作、並べ替えなどの高度な機能をサポートするには、List と Table は作成時に各行の ID を決定する必要があります。これは、ForEach のコンテンツが固定数のビューを生成する必要があることを意味します。

15:12

ForEach で条件付きビューを使用する場合、またはAnyView, SwiftUI は各行がいくつのビューに対応するかを事前に知ることができず、すべてのビューを作成することしかできません。データ量が少ない場合は問題ありませんが、データ量が多い場合には明らかに遅延が発生します。

16:08

フィルタリング ロジックをモデル レイヤーに移動し、ForEach が表示のみを担当できるようにすると、大きなリストの読み込みとスクロールのパフォーマンスが大幅に向上します。

17:35

##詳細

### 使用_printChanges()冗長な依存関係を見つける

SwiftUIはデバッグツールを提供しますSelf._printChanges()、ビューの更新をトリガーした理由を出力できます。

struct ScalableDogImage: View {
    @State private var scaleToFill = false
    var dog: Dog

    var body: some View {
        let _ = Self._printChanges()
        dog.image
            .resizable()
            .aspectRatio(
                contentMode: scaleToFill ? .fill : .fit)
            .frame(maxHeight: scaleToFill ? 500 : nil)
            .padding(.vertical, 16)
            .onTapGesture {
                withAnimation { scaleToFill.toggle() }
            }
    }
}

04:02

キーポイント:

  • let _ = Self._printChanges()これを本文の先頭に配置すると、ビューが更新されるたびに理由がコンソールに出力されます。
  • 印刷@Selfビュー自体の値が変更されたことを示します(つまり、受信dog変更されました)
  • 印刷@scaleToFill急行@Stateプロパティが変更されました
  • このメソッドには先頭にアンダースコアが付いており、デバッグ専用です。 App Store に提出しないでください。

ピクチャービューを見ると、dog.name変更によって更新されると、依存関係のスコープが大きすぎることがわかります。バンドルdogに変更しますdog.imageその後、名前を変更してもイメージの更新はトリガーされません。

08:46

struct DogView: View {
    @Environment(\.isPlayTime) private var isPlayTime
    var dog: Dog

    var body: some View {
        DogHeader(name: dog.name, breed: dog.breed)
        ScalableDogImage(dog.image)
        DogDetailView(dog)
        LetsPlayButton()
            .disabled(dog.isTired)
    }
}

キーポイント:

  • ScalableDogImage今は受け取るだけImage、全体に依存しなくなりました。Dog
  • DogHeader孤立して受け取るだけnameそしてbreed
  • 各サブビューの依存関係が呼び出しサイトで明確に表示されます。

同期ブロッキングを非同期ロードに変更します。

// 问题代码:init 中同步获取数据
struct DogRootView: View {
    @State private var model = FetchModel()

    var body: some View {
        DogList(model.dogs)
    }
}

@Observable class FetchModel {
    var dogs: [Dog]

    init() {
        fetchDogs() // 同步阻塞
    }

    func fetchDogs() {
        // Takes a long time
    }
}

12:22

// 修复后:异步获取
struct DogRootView: View {
    @State private var model = FetchModel()

    var body: some View {
        DogList(model.dogs)
            .task { await model.fetchDogs() }
    }
}

@Observable class FetchModel {
    var dogs: [Dog]

    init() {}

    func fetchDogs() async {
        // Takes a long time
    }
}

12:23

キーポイント:

  • .taskモディファイアはビューが表示されるときに非同期タスクを開始し、ビューの作成自体はブロックされません。
  • fetchDogs()としてマークされていますasync、内部で使用できますawaitネットワークリクエストを行う
  • @Observable属性の読み取りのみを自動的に追跡します。dogs変更が発生するとビューの更新がトリガーされます

リスト内のビューの可変数を避ける

// 问题:条件视图导致 List 需要创建所有视图来确定 identity
List {
    ForEach(dogs) { dog in
        if dog.likesTennisBalls {
            DogCell(dog)
        }
    }
}

16:08

// 修复:过滤逻辑移到模型层
List {
    ForEach(tennisBallDogs) { dog in
        DogCell(dog)
    }
}

17:35

キーポイント:

  • リストはすべての行の ID を事前に知っている必要があります
  • ForEach のコンテンツに条件付き分岐が含まれる場合、SwiftUI は行数を決定するために各要素の可能なすべてのビューを作成する必要があります。
  • モデル層にフィルタリングを置きます。 ForEach によって受信されたデータはすでにフィルター処理されており、行ごとに 1 つのビューのみが生成されます。
  • グループ化されたリストの場合、ネストされた ForEach が推奨され、SwiftUI はこの構造を特別に処理します。
// Sectioned List 是嵌套 ForEach 的合法用法
struct DogsByToy: View {
    var model: DogModel

    var body: some View {
        List {
            ForEach(model.dogToys) { toy in
                Section(toy.name) {
                    ForEach(model.dogs(toy: toy)) { dog in
                        DogCell(dog)
                    }
                }
            }
        }
    }
}

18:25

テーブルの簡略化された初期化

iOS 17 と macOS Sonoma では、簡素化されたテーブル初期化メソッドが導入されています。

// iOS 16 的方式
struct DogTable: View {
    var dogs: [Dog]

    var body: some View {
        Table(of: Dog.self) {
            // Columns
        } rows: {
            ForEach(dogs) { dog in
                TableRow(dog)
            }
        }
    }
}

19:21

// iOS 17 的新方式,自动创建 TableRow
struct DogTable: View {
    var dogs: [Dog]

    var body: some View {
        Table(of: Dog.self) {
            // Columns
        } rows: {
            ForEach(dogs)
        }
    }
}

19:22

キーポイント:

  • データ要素ごとに自動的に作成される新しい初期化メソッドTableRow
  • これにより、行ごとにビューが 1 つだけになり、アイデンティティ計算がより効率的になります。
  • この API は、Table で最初にサポートされたシステム バージョンとの下位互換性があります。
  • セマンティックの変更に注意してください:TableRowさまざまなデータが使用されます(たとえば、dog.bestFriend)、iOS 17 では ID は以下に基づきます。dogそれよりもdog.bestFriend

重要ポイント

1.複雑なリストに追加する_printChanges()診断

データの更新時にリストのスクロールが止まったり、インターフェイスがちらついたりする場合は、次のことを追加してくださいlet _ = Self._printChanges()、実行後のコンソール出力を観察します。プロパティを変更すると、関連性のない多数のビューが更新されることがわかった場合は、依存関係を 1 つずつ最小限の範囲に減らします。入り口はLLDBですexpression Self._printChanges()または、コードに一時的に追加します。

2.使用@Observable交換する@ObservedObject + @Published

新しいプロジェクトで直接使用します@Observable class Modelビューでは不要になったデータ モデルを定義します。@ObservedObjectまたは@StateObject。SwiftUI は、実際に読み取られるプロパティを自動的に追跡し、それらのプロパティに実際に依存するビューのみを更新します。入り口は、import Observation、そしてそれを置きますObservableObjectに変更します@Observable

3.すべてのリスト/テーブルの ForEach コンテンツを確認します

リストとテーブルをチェックして、ForEach のクロージャーに次のものが含まれていないことを確認します。if状態、AnyViewまたはネストされた可変構造。条件付きフィルタリングがある場合は、それを ViewModel に移動します。入り口はすべてを見つけることですList { ForEach(...) }そしてTable { ForEach(...) }、各要素に対してビューが 1 つだけ生成されることを確認します。

4.置く.taskデータをロードするデフォルトの方法として

あなたがそうであればonAppear同期メソッドを呼び出してデータを取得するか、init()ネットワーク要求を行うときは、次のように変更します。.task { await model.load() }非同期モード。これにより、インターフェイスのフリーズが防止され、ビューが消えたときに SwiftUI がタスクを自動的にキャンセルできるようになります。入り口は、同期読み込み方法を次のように変更することです。asyncを追加します。.task修飾子。

5.使用inspectorColumnWidthレイアウトのデバッグ中にパフォーマンスをチェック

インスペクターとシートのカスタム表示には、ビューの再構築も含まれます。インスペクターに大量のデータを表示している場合は、インスペクターのコンテンツ ビューが同じ依存関係の最小化原則に従っていることを確認してください。

関連セッション

コメント

GitHub Issues · utterances