SwiftUI — navigationDestination

区别 #

navigationDestination 在 SwiftUI 中确实有三种不同的定义方式。它们主要区别在于如何定义目标视图以及如何传递和管理数据。这三种方式分别是:

  1. 基于 BindingnavigationDestination(isPresented:destination:)
  2. 基于 OptionalnavigationDestination(item:destination:)
  3. 基于数据类型的 navigationDestination(for:destination:)

接下来我们逐一进行讲解,包括用法以及它们的区别。


1. 基于 BindingnavigationDestination(isPresented:destination:) #

此方式通过绑定布尔值(BoolBinding)控制目标视图是否呈现。这是一种比较简单的导航方式,适合用于单独条件控制的导航。


用法 #

struct BindingNavigationExample: View {
    @State private var isDetailViewVisible = false // 控制目标视图的显示状态

    var body: some View {
        NavigationStack {
            VStack {
                Button("Go to Detail View") {
                    isDetailViewVisible = true // 点击切换布尔值,触发导航
                }
            }
            // 使用 isPresented 绑定来定义目标视图
            .navigationDestination(isPresented: $isDetailViewVisible) {
                DetailView()
            }
        }
    }
}

struct DetailView: View {
    var body: some View {
        Text("This is the Detail View")
            .font(.largeTitle)
    }
}

特点 #

  1. 触发逻辑: 目标视图的呈现逻辑由布尔值控制。
  2. 直接控制: 开发者通过显式的布尔值赋值控制导航的开启与关闭。
  3. 适合场景: 简单的场景,比如只需要在满足条件时,显示一个固定内容的目标视图。
  4. 导航表现: 一旦 isDetailViewVisible 改变为 true,目标视图会被立即显示。

2. 基于 OptionalnavigationDestination(item:destination:) #

此方式依赖于可选数据 (Optional) 来控制何时展示目标视图。如果可选值为空,则不导航;如果可选值有内容,则导航到相应视图。


用法 #

struct OptionalNavigationExample: View {
    @State private var selectedItem: String? = nil // 控制导航的可选体

    var body: some View {
        NavigationStack {
            VStack {
                Button("Go to Detail for Item 1") {
                    selectedItem = "Item 1" // 赋值触发导航
                }
                Button("Go to Detail for Item 2") {
                    selectedItem = "Item 2" // 赋值触发不同的导航内容
                }
            }
            // 使用 item:destination 定义导航目标
            .navigationDestination(item: $selectedItem) { item in
                DetailItemView(item: item)
            }
        }
    }
}

// 目标视图绑定到每个可选值
struct DetailItemView: View {
    let item: String

    var body: some View {
        Text("Detail View for \(item)")
            .font(.largeTitle)
    }
}

特点 #

  1. 触发逻辑: 基于可选值的状态控制,如果可选值变为非空,则展示目标视图。
  2. 动态数据支持: 每次传递的 item 都可以动态变更,适用于类似选择列表的场景。
  3. 适合场景: 带有单个动态数据的导航逻辑,如展示某个被选中的项目详情。
  4. 导航表现: 修改 selectedItem 会立刻触发导航到相应目标视图。

3. 基于数据类型的 navigationDestination(for:destination:) #

此方式直接根据导航路径中的某种类型的对象确定目标视图。它是 NavigationStack 的核心功能,支持更加复杂的路径管理(如递归导航或多类型导航)。


用法 #

struct TypeBasedNavigationExample: View {
    @State private var path: [Int] = [] // 管理导航路径的数组

    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                Button("Go to Detail 1") {
                    path.append(1) // 在路径中添加一个 Int
                }
                Button("Go to Detail 2") {
                    path.append(2) // 在路径中添加另一个 Int
                }
            }
            .navigationDestination(for: Int.self) { value in
                Text("Detail View for \(value)") // 根据路径值动态生成视图
                    .font(.largeTitle)
            }
        }
    }
}

特点 #

  1. 触发逻辑: 通过路径数组中添加或删除某种类型的值来管理导航。
  2. 复杂路径支持: 支持递归导航和多类型值的联合处理。
  3. 适合场景: 更复杂的导航,例如你需要同时处理多个对象类型(如 IntString)的动态目标视图。
  4. 导航表现: 修改 path 数组会触发导航栈变化。

4. 三种方式的对比 #

特性基于 Binding (isPresented)基于 Optional (item)基于数据类型 (for)
导航触发逻辑布尔值开关直接控制导航是否发生可选值有内容时触发导航修改导航路径数组触发导航
目标视图传递的数据没有数据或通过局部状态传递传递非空的 item自动绑定到路径中的对象
适用场景简单条件下触发导航需要匹配动态内容,且内容是单一的需要支持复杂的动态路径和嵌套导航
复杂性支持简单直接,适合初学者适中,支持动态数据传递最灵活,适合高级路径管理
多类型视图支持不支持部分支持单独的 Optional 类型支持多种类型的导航目标 for: T.self
常见用法单一按钮触发导航列表选择项导航,导航到细节或弹出视图无限导航、多类型数据导航、多视图复杂组织

5. 综合示例:三种方式同时使用 #

如果需要在项目中灵活使用上述三种导航方式,可以这么做:

struct MixedNavigationExample: View {
    @State private var isSimpleViewPresented = false
    @State private var selectedItem: String? = nil
    @State private var path: [AnyHashable] = []

    var body: some View {
        NavigationStack(path: $path) {
            VStack(spacing: 20) {
                // 基于 `Binding` 的导航
                Button("Show Simple View") {
                    isSimpleViewPresented = true
                }

                // 基于 `Optional` 的导航
                Button("Show Detail for Item A") {
                    selectedItem = "Item A"
                }
                Button("Show Detail for Item B") {
                    selectedItem = "Item B"
                }

                // 基于路径管理的导航
                Button("Go to Dynamic Path") {
                    path.append("Dynamic Path")
                }
                
                Button("Go to Int Path") {
                    path.append(42)
                }
            }
            .navigationDestination(isPresented: $isSimpleViewPresented) {
                Text("This is the Simple View")
            }
            .navigationDestination(item: $selectedItem) { item in
                Text("Detail for \(item)")
            }
            .navigationDestination(for: String.self) { value in
                Text("Path for String: \(value)")
            }
            .navigationDestination(for: Int.self) { value in
                Text("Path for Int: \(value)")
            }
        }
    }
}

6. 总结 #

区别: #

  • isPresented:
    • 最简单的方式,直接布尔值控制,十分适合单一导航场景。
  • item:
    • 动态处理单个数据内容的导航,适合选择项导航。
  • for:
    • 最强大的数据绑定方式,支持复杂、多层的路径式导航,适合构建灵活的导航逻辑。

用哪种方式取决于你应用需求的复杂性以及需要传递的数据。

位置要求 #

在 SwiftUI 中,navigationDestination 的位置决定了如何响应导航事件以及如何定义目标视图。虽然 navigationDestination 通常放在父级(如 NavigationStack)的视图结构中,但它的具体位置取决于你需要响应的导航触发方式。以下将详细说明 navigationDestination 应该放在哪、如何与 List 或其他视图配合使用,以及其行为的最佳实践。


1. navigationDestination 的最佳位置 #

navigationDestination 的修饰符应该始终被放置在 NavigationStack 内部结构中,但不一定必须直接紧贴某个特定视图(例如 List)。其规则如下:

  1. 与导航路径绑定的 path:通常定义在 NavigationStack 的视图树中,以便响应整个导航上下文内的路径变化。
  2. 全局定义目标视图navigationDestination 决定了当导航路径触发某种类型的路径值(如 IntString)时,渲染哪个目标视图,但目标并不依赖特定触发元素(如列表项或按钮)。

它可以配合 List 一起使用,但也可以配合 Button 或其他视图绑定路径变化进行导航。


2. 配合 List 动态导航的示例 #

如果你有一个 List,比如展示一组列表项,然后点击列表项跳转到详情页,navigationDestination 可以用来定义目标详情视图。

示例代码:结合 List 使用 navigationDestination #

struct ListNavigationExample: View {
    @State private var items = ["Apple", "Banana", "Cherry"]  // 数据源
    @State private var path: [String] = []                   // 导航路径

    var body: some View {
        NavigationStack(path: $path) { // 定义主 NavigationStack
            List(items, id: \.self) { item in
                Button(item) { // 点击触发导航
                    path.append(item) // 动态更新路径
                }
            }
            // 定义目标视图
            .navigationDestination(for: String.self) { item in
                VStack {
                    Text("Detail for \(item)")
                        .font(.largeTitle)
                    Button("Go Back") {
                        path.removeLast() // 从路径中移除当前目标,返回上一级
                    }
                }
                .padding()
            }
            .navigationTitle("Fruits") // 设置主页面标题
        }
    }
}

解释:关键点 #

  1. 导航路径@State var path):

    • path 作为路径绑定,动态管理当前导航堆栈的状态。
    • List 点击某个项目时,将目标值(如 "Apple")添加到路径中。
  2. navigationDestination(for: String.self)

    • 表示响应路径中包含的 String 类型数据。
    • 当路径中新的 String 值被添加时,目标视图会自动显示。
  3. 按钮绑定到路径变更

    • Button(item) 的点击事件会更新路径,触发导航。
  4. 为什么不用直接在 List 中放 NavigationLink

    • NavigationLink 是更简单的一次性导航方式,而 navigationDestination 针对动态路径绑定,适合更复杂的场景(如多层导航、多类型值)。

示例 2:结合多类型导航的 List #

假如你有一个包含不同类型(如 StringInt)的数据列表,并需要跳转到不同的目标视图,可以这样实现。

struct MultiTypeListNavigationExample: View {
    @State private var items: [AnyHashable] = ["Apple", 42, "Banana", 100]
    @State private var path: [AnyHashable] = []

    var body: some View {
        NavigationStack(path: $path) {
            List(items, id: \.self) { item in
                Button("\(item)") {
                    path.append(item)
                }
            }
            // 为 String 类型定义目标视图
            .navigationDestination(for: String.self) { value in
                VStack {
                    Text("Detail for String: \(value)")
                    Button("Back to List") {
                        path.removeAll()
                    }
                }
            }
            // 为 Int 类型定义目标视图
            .navigationDestination(for: Int.self) { value in
                VStack {
                    Text("Detail for Number: \(value)")
                    Button("Back to List") {
                        path.removeAll()
                    }
                }
            }
        }
    }
}

关键点: #

  1. AnyHashable 的路径支持多种类型:

    • path 定义为 [AnyHashable],可以存储不同类型的值。
    • 使用 navigationDestination 针对每个类型分别定义独立目标视图。
  2. 适合复杂的多类型导航需求:

    • 每个列表项跳转到不同目标(如展示文字或数字详情)。
    • 可扩展支持新类型目标视图。

3. 如果不使用 List 的场景:任意触发导航 #

navigationDestination 不一定需要放在 List 的上下文内,它可以用于任何导航场景,比如按钮触发导航、程序逻辑导航等。

示例:通过普通按钮触发 #

struct ButtonNavigationExample: View {
    @State private var path: [String] = [] // 路径管理
    
    var body: some View {
        NavigationStack(path: $path) {
            VStack(spacing: 20) {
                Button("Go to Page 1") {
                    path.append("Page 1")
                }
                Button("Go to Page 2") {
                    path.append("Page 2")
                }
            }
            .navigationDestination(for: String.self) { page in
                VStack {
                    Text("Welcome to \(page)")
                    Button("Back to Home") {
                        path.removeAll()
                    }
                }
            }
        }
    }
}

4. 放置 navigationDestination 的最佳实践 #

关于 navigationDestination 应该放在什么位置,可以通过下面的规则判断:

4.1 navigationDestination 通常应全局放置在 NavigationStack #

  • 优点: 这种放置方式让导航逻辑独立于视图结构,与触发事件分离,增强了可扩展性。
  • 如果需要新增导航入口,只需要修改路径的定义,不用调整目标视图绑定。

4.2 是否一定要与 List 搭配? #

  • 不一定navigationDestination 只是用于定义导航触发的数据类型及目标视图。
  • 触发导航的元素可以是 List,但也可以是 Button 或其他任何视图事件。

4.3 动态导航与静态导航的选择 #

  • 如果导航目标是静态的(例如只需简单跳转某个子页面),可以直接使用 NavigationLink
  • 如果目标导航是动态的(基于路径值决定目标视图),推荐使用 navigationDestination

5. 两种导航方式对比 #

特性NavigationLinknavigationDestination
触发方式直接绑定在按钮、列表等位置响应路径(如 path)的变化
复杂导航支持支持简单跳转,通常适用于静态场景支持动态导航,路径值可以完成多目标路由
视图与逻辑耦合耦合程度较高,因为不同视图中嵌入逻辑分离,只需在全局或主 NavigationStack 内定义
适合场景静态场景,固定目标动态场景,基于数据驱动

6. 总结 #

回答问题:navigationDestination 应该放在什么位置?需要放在 List 中吗?

结论: #

  • navigationDestination 不需要依赖 List,它是定义目标视图映射的全局工具。
  • 它通常定位在 NavigationStack 中,对导航路径的值变化进行响应。
  • 如果你的数据在 List 中(如动态生成的条目),可以结合 ListnavigationDestination 使用,但这只是一个实现场景。
  • 适合使用于:动态导航路径、多层次目标逻辑、复杂目标映射等高级场景。

navigationDestination(for:)NavigationLink 结合使用可以实现更灵活的导航管理。让我通过示例来说明:

1. 基本用法 #

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                // 使用 NavigationLink 并指定值
                NavigationLink("去详情页", value: "详情数据")
                NavigationLink("去设置页", value: true)
            }
            // 处理不同类型的导航目标
            .navigationDestination(for: String.self) { text in
                DetailView(text: text)
            }
            .navigationDestination(for: Bool.self) { _ in
                SettingsView()
            }
        }
    }
}

2. 使用自定义类型 #

// 定义导航目标类型
struct Item: Hashable {
    let id: Int
    let title: String
}

struct ContentView: View {
    let items = [
        Item(id: 1, title: "项目1"),
        Item(id: 2, title: "项目2")
    ]
    
    var body: some View {
        NavigationStack {
            List(items, id: \.id) { item in
                NavigationLink(item.title, value: item)
            }
            .navigationDestination(for: Item.self) { item in
                ItemDetailView(item: item)
            }
        }
    }
}

struct ItemDetailView: View {
    let item: Item
    
    var body: some View {
        Text("详情: \(item.title)")
    }
}

3. 多级导航 #

// 定义不同的导航类型
enum NavigationType: Hashable {
    case detail(Item)
    case settings
    case profile
}

struct ContentView: View {
    let items = [
        Item(id: 1, title: "项目1"),
        Item(id: 2, title: "项目2")
    ]
    
    var body: some View {
        NavigationStack {
            List {
                Section("项目") {
                    ForEach(items, id: \.id) { item in
                        NavigationLink(
                            item.title,
                            value: NavigationType.detail(item)
                        )
                    }
                }
                
                Section("设置") {
                    NavigationLink(
                        "设置",
                        value: NavigationType.settings
                    )
                    NavigationLink(
                        "个人资料",
                        value: NavigationType.profile
                    )
                }
            }
            .navigationDestination(for: NavigationType.self) { type in
                switch type {
                case .detail(let item):
                    ItemDetailView(item: item)
                case .settings:
                    SettingsView()
                case .profile:
                    ProfileView()
                }
            }
        }
    }
}

4. 条件导航 #

struct ContentView: View {
    @State private var selectedItem: Item?
    @State private var showSettings = false
    
    var body: some View {
        NavigationStack {
            List {
                // 使用按钮触发导航
                Button("打开设置") {
                    showSettings = true
                }
                
                // 使用 NavigationLink
                NavigationLink("选择项目1", value: Item(id: 1, title: "项目1"))
            }
            .navigationDestination(for: Item.self) { item in
                ItemDetailView(item: item)
            }
            .navigationDestination(isPresented: $showSettings) {
                SettingsView()
            }
        }
    }
}

5. 深层链接 #

struct ContentView: View {
    @State private var path = NavigationPath()
    
    var body: some View {
        NavigationStack(path: $path) {
            List {
                NavigationLink("第一层", value: "level1")
            }
            .navigationDestination(for: String.self) { level in
                if level == "level1" {
                    VStack {
                        Text("第一层视图")
                        NavigationLink("进入第二层", value: "level2")
                    }
                } else if level == "level2" {
                    VStack {
                        Text("第二层视图")
                        Button("返回根视图") {
                            path.removeLast(path.count)
                        }
                    }
                }
            }
        }
    }
}

6. 组合使用示例 #

// 定义多个导航类型
enum Route: Hashable {
    case item(Item)
    case category(String)
    case search(String)
}

struct ContentView: View {
    @State private var searchText = ""
    @State private var path = NavigationPath()
    
    let items = [
        Item(id: 1, title: "项目1"),
        Item(id: 2, title: "项目2")
    ]
    
    var body: some View {
        NavigationStack(path: $path) {
            List {
                Section("搜索") {
                    TextField("搜索", text: $searchText)
                    Button("搜索") {
                        path.append(Route.search(searchText))
                    }
                }
                
                Section("分类") {
                    NavigationLink("分类A", value: Route.category("A"))
                    NavigationLink("分类B", value: Route.category("B"))
                }
                
                Section("项目") {
                    ForEach(items, id: \.id) { item in
                        NavigationLink(item.title, value: Route.item(item))
                    }
                }
            }
            .navigationDestination(for: Route.self) { route in
                switch route {
                case .item(let item):
                    ItemDetailView(item: item)
                case .category(let category):
                    CategoryView(category: category)
                case .search(let query):
                    SearchResultView(query: query)
                }
            }
        }
    }
}

// 子视图
struct CategoryView: View {
    let category: String
    
    var body: some View {
        VStack {
            Text("分类: \(category)")
            NavigationLink("查看详情", value: Route.item(Item(id: 99, title: "分类项目")))
        }
    }
}

struct SearchResultView: View {
    let query: String
    
    var body: some View {
        Text("搜索结果: \(query)")
    }
}

7. 最佳实践 #

  1. 类型安全
// 使用枚举定义导航路由
enum Route: Hashable {
    case detail(Item)
    case settings
}

// 在视图中使用
.navigationDestination(for: Route.self) { route in
    switch route {
    case .detail(let item):
        DetailView(item: item)
    case .settings:
        SettingsView()
    }
}
  1. 路径管理
@State private var path = NavigationPath()

// 使用路径管理导航状态
NavigationStack(path: $path) {
    // 内容
}
  1. 条件导航
@State private var selectedItem: Item?

// 使用可选值处理条件导航
if let item = selectedItem {
    NavigationLink(
        item.title,
        value: Route.detail(item)
    )
}

8. 注意事项 #

  1. 确保导航类型实现了 Hashable 协议
  2. 合理组织导航层级,避免过深的嵌套
  3. 考虑使用 NavigationPath 管理复杂的导航状态
  4. 适当使用类型安全的枚举来管理不同的导航目标
  5. 注意内存管理,避免循环引用

通过这些示例,你可以看到 navigationDestination(for:)NavigationLink 的结合使用提供了强大而灵活的导航管理能力,适合处理各种复杂的导航场景。

完整案例:如何触发导航 #

假设你在开发一个待办事项应用(Reminders App),有以下需求:

  1. 主屏幕是 ReminderList 的列表(比如 “工作”、“个人事项” 等)。
  2. 点击某个列表后,导航到 CreateSectionView 页面,用于创建某个分类的待办事项。

可以通过 .navigationDestination(for:) 实现导航绑定:

import SwiftUI

struct ReminderList: Identifiable, Hashable {
    let id: UUID = UUID()
    let name: String
}

struct ContentView: View {
    @State private var selectedList: ReminderList?
    
    let reminderLists = [
        ReminderList(name: "Work"),
        ReminderList(name: "Personal"),
        ReminderList(name: "Groceries")
    ]
    
    var body: some View {
        NavigationStack {
            List(reminderLists) { reminderList in
                NavigationLink(value: reminderList) {
                    Text(reminderList.name)
                }
            }
            .navigationTitle("Reminders")
            .navigationDestination(for: ReminderList.self, destination: CreateSectionView.init)
        }
    }
}

struct CreateSectionView: View {
    let reminderList: ReminderList

    init(_ reminderList: ReminderList) {
        self.reminderList = reminderList
    }

    var body: some View {
        Text("Create items for \(reminderList.name)!")
            .font(.title)
    }
}

功能分析 #

  1. ReminderList 数据模型
  • 数据类型 ReminderList 是一个标识符类型(Identifiable),用来表示不同的待办事项分类(如“工作”、“个人”)。
  1. List 和 NavigationLink
  • NavigationLink(value:) 用来绑定分类数据(ReminderList)。
  • 用户点击某个分类时,会将那个 ReminderList 放入 NavigationStack 的导航路径中。
  1. navigationDestination(for:)
  • for: ReminderList.self

  • 表示导航系统会匹配 ReminderList 类型的数据。

  • destination: CreateSectionView.init

  • 表示每当导航系统遇到 ReminderList 数据时,会传递这个数据给 CreateSectionView.init,并导航到这个目标页面。

  1. CreateSectionView 页面
  • CreateSectionView 根据传入的 ReminderList 显示不同的内容。

代码运行效果 #

  1. 主页面列出 ReminderList 的分类(如“Work”、“Personal”、“Groceries”)。
  2. 点击某个分类(如“Work”),将导航到 CreateSectionView 页面,并显示 This is Create items for Work!

总结 #

这行代码的核心作用是基于 navigationDestination(for:) 设置目标视图,实现类型化的页面导航。

关键点: #

  • for: ReminderList.self

  • 指定数据类型为 ReminderList,只有导航值是 ReminderList 类型的实例时,才导航到目标视图。

  • destination: CreateSectionView.init

  • 指定目标页面为 CreateSectionView,并将数据绑定到目标视图。

这种写法非常适合创建基于数据驱动的动态导航,尤其在使用 NavigationStack 时表现得更加灵活和清晰。

本文共 5757 字,上次修改于 Jan 5, 2025