WWDC Quick Look 💓 By SwiftGGTeam
The SwiftUI cookbook for navigation

The SwiftUI cookbook for navigation

元の動画を見る

ハイライト

iOS 16 と macOS Ventura の新しい SwiftUINavigationStackNavigationSplitView、価値主導型NavigationLinknavigationDestination永続的なナビゲーション状態により、開発者は個別のブール バインディングではなくデータを使用して、プッシュ、列、ディープ リンク、状態の復元を制御できるようになります。

主要内容

古い SwiftUI ナビゲーション API は、NavigationLinkターゲットビューを直接中央に移動します。単純なジャンプは機能しますが、プログラムによるナビゲーションを実行したい場合は、リンクごとに個別のバインディングを準備する必要があります。セッションはこの問題を 01:48 に直接指摘しました。特定のリンクのビューを表示するには、対応するリンクを設定する必要があります。item.showDetail、ナビゲーション状態を多くの場所に分割します。

新しいアイデアは、コンテナへのバインディングを持ち上げることです。NavigationStack持ってくださいpath、このセットは、現在のスタック上にプッシュされたすべてのデータ値を表します。クリック値主導型NavigationLink値はパスに追加されます。このコードは、ディープ リンクへのパスを変更したり、ルート ビューに戻ったりすることができます (02:06)。

セッションでは、実行例としてレシピ アプリを使用します。最初に 1 列のプッシュ/ポップ構造を示し、次に 3 列を示します。NavigationSplitView、最後に分割ビューとスタックを組み合わせて使用​​しますCodableそしてSceneStorageナビゲーション状態を保存します。

詳細

単一列スタック: プッシュをパス データに変換します

(06:05) 最初のレシピは、iPhone の設定や Apple Watch の「探す」に似た 1 列のナビゲーションです。ルート ビューにはレシピ カテゴリがリストされます。レシピをクリックした後、関連するレシピをクリックし続けて、新しい詳細ページをスタックにプッシュできます。

公式コードの中核は次の 3 つです。NavigationStack(path:)NavigationLink(value:)navigationDestination(for:)

struct PushableStack: View {
    @State private var path: [Recipe] = []
    @StateObject private var dataModel = DataModel()

    var body: some View {
        NavigationStack(path: $path) {
            List(Category.allCases) { category in
                Section(category.localizedName) {
                    ForEach(dataModel.recipes(in: category)) { recipe in
                        NavigationLink(recipe.name, value: recipe)
                    }
                }
            }
            .navigationTitle("Categories")
            .navigationDestination(for: Recipe.self) { recipe in
                RecipeDetail(recipe: recipe)
            }
        }
        .environmentObject(dataModel)
    }
}

キーポイント:

  • @State private var path: [Recipe] = []現在スタック上にあるレシピデータを保存します。この例では、スタックには表示のみが行われます。Recipe、したがってパスは次のようになります[Recipe]
  • NavigationStack(path: $path)スタック状態をバインディングとして公開します。コード変更path、UIの後にプッシュまたはポップが続きます。 -NavigationLink(recipe.name, value: recipe)ターゲット ビューを直接記述することはなくなり、1 つだけを送信しますRecipe価値。 -.navigationDestination(for: Recipe.self)声明Recipe値をマッピングする必要がありますRecipeDetail
  • セッションは 09:20 に、スタックが複数のデータ型を表示したい場合は型消去を使用できることを思い出させました。NavigationPath

この構造の利点は、それが明確であることです。リンクを 1 つずつトリガーすることなく、Apple Pie および Pie Crust にディープリンクします。 2つ入れるだけRecipeそれをパスに置くと、スタックは 2 レベルの詳細ページを復元します。ルートビューに戻り、パスをクリアします (09:40)。

3 列構造: 選択状態によって分割ビューを制御します

(10:40) 2 番目のレシピは、Mail や Notes のような複数列構造です。最初の列はカテゴリを選択し、2 番目の列はレシピを選択し、3 番目の列は詳細を表示します。

ここでのポイントは、NavigationSplitViewそしてList(selection:)協力。価値主導型NavigationLink選択範囲のあるリストに表示されている場合、リンクをクリックすると、対応する選択内容が自動的に更新されます。

struct MultipleColumns: View {
    @State private var selectedCategory: Category?
    @State private var selectedRecipe: Recipe?
    @StateObject private var dataModel = DataModel()

    var body: some View {
        NavigationSplitView {
            List(Category.allCases, selection: $selectedCategory) { category in
                NavigationLink(category.localizedName, value: category)
            }
            .navigationTitle("Categories")
        } content: {
            List(
                dataModel.recipes(in: selectedCategory),
                selection: $selectedRecipe)
            { recipe in
                NavigationLink(recipe.name, value: recipe)
            }
            .navigationTitle(selectedCategory?.localizedName ?? "Recipes")
        } detail: {
            RecipeDetail(recipe: selectedRecipe)
        }
    }
}

キーポイント:

  • selectedCategory現在選択されているサイドバーのカテゴリを記録します。 -selectedRecipe現在選択されているレシピをコンテンツ列に記録します。 -NavigationSplitView { ... } content: { ... } detail: { ... }3 列構造を定義します。 -List(Category.allCases, selection: $selectedCategory)最初の列の選択状態を読み取り可能および書き込み可能にします。 -NavigationLink(category.localizedName, value: category)提出するCategory価値; List の選択タイプが一致するため、SwiftUI が更新されますselectedCategory
  • 2 番目のリストが使用されますdataModel.recipes(in: selectedCategory)最初の列の選択に基づいてレシピを表示します。 -RecipeDetail(recipe: selectedRecipe)2 番目の列を使用して、「更新の詳細」領域を選択します。

この構造により、大画面でのモーダル切り替えが軽減されます。ユーザーはカテゴリ、リスト、詳細を同時に見ることができます。 13:00からセッション説明、NavigationSplitViewiPhone または iPad の Slide Over の単一列スタックに自動的に適応し、選択の変更は対応するプッシュとポップに変換されます。

分割ビューとスタック: 詳細列を押し続けます

(14:10) 3 番目のレシピは 2 種類のコンテナを組み合わせています。カテゴリ リストは引き続き左側にあり、レシピ グリッドは最初に右側の詳細領域に表示されます。グリッド内のレシピをクリックした後、右側の詳細領域にレシピの詳細をプッシュします。関連するレシピをクリックしてプッシュを続けます。

コードハンドルNavigationStack入れますNavigationSplitView詳細欄。

struct MultipleColumnsWithStack: View {
    @State private var selectedCategory: Category?
    @State private var path: [Recipe] = []
    @StateObject private var dataModel = DataModel()

    var body: some View {
        NavigationSplitView {
            List(Category.allCases, selection: $selectedCategory) { category in
                NavigationLink(category.localizedName, value: category)
            }
            .navigationTitle("Categories")
        } detail: {
            NavigationStack(path: $path) {
                RecipeGrid(category: selectedCategory)
            }
        }
        .environmentObject(dataModel)
    }
}

キーポイント:

  • selectedCategory左側でカテゴリの選択を制御します。 -path: [Recipe]右側の詳細列内のプッシュ スタックを制御します。 -NavigationSplitViewワイドスクリーンのコラム構造を担当。 -NavigationStack(path: $path)詳細列に埋め込まれているため、右側の部分スタックのみが管理されます。 -RecipeGrid(category: selectedCategory)このローカル スタックのルート ビューです。

RecipeGridトラブルに巻き込まれやすい点がもう 1 つあります。navigationDestination置いてはいけないLazyVGridのすべてのリンクにあります。

struct RecipeGrid: View {
    @EnvironmentObject private var dataModel: DataModel
    var category: Category?

    var body: some View {
        if let category = category {
            ScrollView {
                LazyVGrid(columns: columns) {
                    ForEach(dataModel.recipes(in: category)) { recipe in
                        NavigationLink(value: recipe) {
                            RecipeTile(recipe: recipe)
                        }
                    }
                }
            }
            .navigationTitle(category.localizedName)
            .navigationDestination(for: Recipe.self) { recipe in
                RecipeDetail(recipe: recipe)
            }
        } else {
            Text("Select a category")
        }
    }

    var columns: [GridItem] { [GridItem(.adaptive(minimum: 240))] }
}

キーポイント:

  • LazyVGrid必要な場合にのみサブビューをロードします。セッションは 16:19 で明示的に次のように述べています。ListTableLazyVGridこのタイプの遅延コンテナは、すべてのコンテンツを一度にロードしません。
  • 置いた場合navigationDestination遅延サブビューの外層に配置しますNavigationStackたぶん見えないんでしょうね。
  • バンドル.navigationDestination(for: Recipe.self)つけるScrollViewリンク上で、リンクに近く、宛先がスタックによって確実に検出されるようにします。 -NavigationLink(value: recipe)宛先と宛先の種類は両方ともRecipe、タイプが一致した場合にのみプッシュされますRecipeDetail

このコンボは、写真スタイルの iPad または Mac インターフェイスに適合します。ルート レベルの列を使用してレベルの深さを減らし、詳細列内のスタックを使用して継続的なドリルダウンを表示します。

状態リカバリ: データ モデル全体のコピーを避けるために、ナビゲーション モデルを保存します。

(18:12) 最後のレシピはステータスの復元を解決します。ユーザーがアプリを離れて戻ってきたら、元のカテゴリ、グリッド、詳細ページに戻る必要があります。

Session によって提供されるアプローチは 3 つのステップに分かれています。ナビゲーション状態をモデルにカプセル化し、モデルにサポートを提供します。Codable、次に使用しますSceneStorageJSONデータを保存します。

struct UseSceneStorage: View {
    @StateObject private var navModel = NavigationModel()
    @SceneStorage("navigation") private var data: Data?
    @StateObject private var dataModel = DataModel()

    var body: some View {
        NavigationSplitView {
            List(
                Category.allCases, selection: $navModel.selectedCategory
            ) { category in
                NavigationLink(category.localizedName, value: category)
            }
            .navigationTitle("Categories")
        } detail: {
            NavigationStack(path: $navModel.recipePath) {
                RecipeGrid(category: navModel.selectedCategory)
            }
        }
        .task {
            if let data = data {
                navModel.jsonData = data
            }
            for await _ in navModel.objectWillChangeSequence {
                data = navModel.jsonData
            }
        }
        .environmentObject(dataModel)
    }
}

キーポイント:

  • navModel一元的に保存selectedCategoryそしてrecipePath、2 つの状態が矛盾した組み合わせに戻るのを防ぎます。 -@SceneStorage("navigation") private var data: Data?各シーンのナビゲーション状態を SwiftUI に渡して保存します。 -.taskビューが表示されると実行されます。データがすでに存在する場合は、まずデータをデコードして戻しますnavModel
  • for await _ in navModel.objectWillChangeSequenceナビゲーションのモデル変更を監視します。
  • それぞれの変更後、data = navModel.jsonData最新のナビゲーション状態をシーン ストレージに書き込みます。

NavigationModelコーディング部分は重要です。全体を保存するわけではありませんRecipe代わりに、レシピ ID が保存されます。

class NavigationModel: ObservableObject, Codable {
    @Published var selectedCategory: Category?
    @Published var recipePath: [Recipe] = []

    enum CodingKeys: String, CodingKey {
        case selectedCategory
        case recipePathIds
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(selectedCategory, forKey: .selectedCategory)
        try container.encode(recipePath.map(\.id), forKey: .recipePathIds)
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.selectedCategory = try container.decodeIfPresent(
            Category.self, forKey: .selectedCategory)

        let recipePathIds = try container.decode([Recipe.ID].self, forKey: .recipePathIds)
        self.recipePath = recipePathIds.compactMap { DataModel.shared[$0] }
    }
}

キーポイント:

  • selectedCategory直接エンコードできるので、Category安定した小さな列挙です。 -recipePath.map(\.id)パス上の各レシピの ID のみが保存されます。
  • デコードするときは、まず ID リストを復元し、次に共有データ モデルを使用してそれを取得します。Recipe
  • compactMap見つからないレシピは破棄されます。 21:28 のセッション 例: 同期後に別のデバイスがレシピを削除した場合、状態を復元するときにすべての ID がまだ存在すると想定することはできません。
  • これにより、ナビゲーション インテントが保存され、変更される可能性のあるモデル コンテンツがローカル ナビゲーション状態にコピーされることが回避されます。

重要ポイント

  • 対処方法: コンテンツベースのアプリの特定のアイテムに URL ディープ リンクを追加します。
    実行する価値がある理由:NavigationStackパス自体は変数の集合であり、ディープリンクはパス値に直接変換できます。
    開始方法: URL を解析してモデル ID を取得し、モデルを見つけて書き込みます[Recipe]NavigationPathまたは独自のパスの種類。

  • 対処法: iPad の詳細ページをサイドバー + グリッド + 詳細の組み合わせに変更します。
    実行する価値がある理由:NavigationSplitView大きな画面に複数の列を表示し、狭い画面では単一列のスタックに自動的に適応できます。
    開始方法: を使用しますList(selection:)詳細列に埋め込まれたサイドバーの選択を管理しますNavigationStack(path:)

  • 内容: ユーザーがアプリを終了する前の詳細レベルを復元します。
    実行する価値がある理由: セッション ショーSceneStorageナビゲーション JSON のフルパスを保存します。
    開始方法: 選択範囲とパスをカプセル化します。ObservableObject & Codableモデル、安定した ID のみをエンコードしてから使用しますSceneStorage保存Data

  • 内容: 古いブール値バインドされたプログラム ナビゲーションをクリーンアップします。
    実行する価値がある理由: セッションは 24:24 に、バインディングを伴う古いプログラムによるリンクが iOS 16 および対応するシステムから廃止されると説明しました。
    開始方法: 複数入れるisActiveまたはtag/selectionリンクは値主導型のリンクに再構築され、一元的に宣言されます。navigationDestination

関連セッション

コメント

GitHub Issues · utterances