NavigationLink

NavigationLink 可以将任何视图推送到 NavigationStack 上。最简单的,可以为其提供一个字符串作为标题,并提供一个目标视图作为尾随闭包即可。NavigationLink 可以与任何类型的目标视图一起使用(包括自定义视图,或者也可以直接呈现某些文本)。

虽然 sheet()NavigationLink 都可以呈现新视图,但它们的呈现方式存在差异,应该仔细选择:


声明方法

// 写法1:在 NavigationLink 中同时提供【标签】和【目标视图】
NavigationStack {
		// 标签
    NavigationLink("Tap Me") {
				// 目标视图
        Text("Detail View")
    }
}

// 写法2:如果想要用复杂点的视图作为标签,可以在 NavigationLink 中使用两个尾随闭包。例如:
NavigationStack {
    NavigationLink {
				// 目标视图
        Text("Detail View")
    } label: {
				// 标签
        VStack {
            Text("This is the label")
            Image(systemName: "face.smiling")
        }
        .font(.largeTitle)
    }
}

<aside> 💡 SwiftUI 会自动将 NavigationLink 设置为按钮,以便用户知道它们是交互式的。您可以通过将 .buttonStyle(.plain) 应用于 NavigationLink 来禁用此行为。

</aside>

声明方式的缺点

在以上写法中,只要写下了 NavigationLink,即使还没有点击按钮,还没有跳转,这个视图其实已经创建了。某些情况下这对效率是个影响:

// 创建子视图:初始化时打印一条消息
struct DetailView: View {
    var number: Int
    var body: some View {
        Text("Detail View \\(number)")
    }
    init(number: Int) {
        self.number = number
        print("Creating detail view \\(number)")
    }
}

// 创建主视图
NavigationStack {
    List(0..<1000) { i in
        NavigationLink("Tap Me") {
            DetailView(number: i)
        }
    }
}

<aside> 💡

以上代码在滚动时,会看到许多 DetailView 实例正在创建,而且通常不止一次。这使得 Swift 和 SwiftUI 做了比必要的更多的工作。因此当处理动态数据时,SwiftUI 有更好的解决方案:即下面介绍的【链接标签 与 目标视图分离】。

</aside>

移除 List 小箭头

<aside> 💡

注意:当在 List 中使用 NavigationLink 时,会在右侧边缘看到灰色的指示器。如果注释掉 NavigationLink ,例如换成 ForEach ,将看到指示器消失。所以以下技巧只需要对 List 下的 NavigationLink 适用;对 ScrollView 下的 NavigationLink 就没必要。

</aside>

SwiftUI 没有提供关闭或隐藏指示箭头的选项。要处理这个问题,通常用两层的 ZStack 来实现导航链接。较低一层是真实内容,上层则是空视图。 NavigationLink 现在设定给空视图,避免 iOS 渲染指示箭头。修改完后,最后在 NavigationLink 视图上加上 opacity 修饰符,设置成0。

List(articles) { article in
    ZStack {
			MissionListCellView(mission: mission)
			NavigationLink {
					MissionView(mission: mission, astronauts: astronauts)
			} label: {
					EmptyView()
			}
			.opacity(0)
    }
}

NavigationDestination

<aside> 💡

记得:当用 navigationDestination 进行导航,在二级视图中,不能够再用 NavigationStack ,否则会崩溃。

</aside>

链接与目标视图分离

在简单的导航中,我们在 NavigationLink 中同时提供【标签视图】和【目标视图】,这种做法一旦定义了 NavigationLink ,就会生成后面的二级视图。所以当遇到循环列表多的情况,就会一下创建很多二级视图,造成资源浪费。

如果您不需要高度自定义的导航,并且仅支持 iOS 16 或更高版本。强烈建议使用 navigationDestination() 这种链接与目标视图分离的方式,因为它允许 SwiftUI 延迟实例化您的目标视图,减少性能损耗。具体做法如下:

1. 为 NavigationLink 添加附加值

//例如,此结构包含一个 UUID、一个字符串和一个整数:
struct Student {
    var id = UUID()
    var name: String
    var age: Int
}

// 如果我们想让该结构符合 Hashable ,只需添加 :Hashable
struct Student: Hashable {
    var id = UUID()
    var name: String
    var age: Int
}

// 现在 Student 就符合 Hashable,它可以与 NavigationLink 和 navigationDestination() 一起使用,像整数或字符串一样。
// Swift 大量使用 Hashable。例如当使用 Set 而不是数组时,放入其中的内容都必须符合 Hashable 协议。这就是集合比数组更快的原因。
// 写法1: 只用文字
NavigationLink("链接文案", value: 1)

// 写法2: 跟一个View
NavigationLink(value: book) {
	HStack {
			EmojiRatingView(rating: book.rating)
	}
}

// 例子:创建一个由 100 个数字组成的 List ,每个数字都附加到一个导航链接作为其表示值(我们告诉 SwiftUI 想要导航到一个数字):
NavigationStack {
    List(0..<100) { i in
        NavigationLink("Select \\(i)", value: i)
    }
}

2. 添加 navigationDestination 接收值

NavigationStack 中(也就是列表的后面),加一个 navigationDestination() 修饰符,告诉它收到数据后要做什么。

//常规写法:
NavigationStack {
    List(0..<100) { i in
        NavigationLink("Select \\(i)", value: i)
    }
    .navigationDestination(for: Int.self) { item in
        Text("You selected \\(selection)")
    }
}

// 可以这样理解:等于区分开 NavigationLink、value、navigationDestination... 这几个东西。代表的意思是,首先告诉 SwiftUI 在点击 NavigationLink 时,导航到附加值 value,但导航到附加值会怎么样展示呢?它应该是文本、VStack 、自定义视图还是什么东西呢?这就是 navigationDestination 修饰符的用武之地,它里面的 for 属性代表“当你被要求导航到某类型数据时,应该展示什么...”

//变体写法:可以做判断
NavigationStack{
		VStack(spacing: 10) {
		    NavigationLink("导航按钮-去模版2", value: 1)
        NavigationLink("导航按钮-去模版3", value: 2)
		}
		.navigationDestination(for: Int.self) { item in
        if item == 1{
		        DetailView2(number: item)
	      }else{
            DetailView3(number: item)
        }   
		}
}

// 此时,当 SwiftUI 尝试导航到任何 Int 值时,就会在 selection 常量中提供该值,并且需要返回正确的视图来显示它。注意:
// - for 参数后面填的是:某种类型。当我们想指代这种类型的时候,要加上 `.self` ,例如 `Int.self`
// - item 代表的是输入的 `NavigationLink` 导航的 `value` 值,因此还可以用于做 if 判断

3. 处理多个导航类型

如果有多种不同类型的数据要导航,只需添加几个 navigationDestination 修饰符即可,每种类型一个。只要他们都符合 Hashable 协议。

struct ContentView: View {
    var body: some View {
        NavigationStack {
            List {
                NavigationLink("Show an integer", value: 42)
                NavigationLink("Show a string", value: "Hello, world!")
                NavigationLink("Show a Double", value: Double.pi)
            }
            .navigationDestination(for: Int.self) { Text("Received Int: \\($0)") }
            .navigationDestination(for: String.self) { Text("Received String: \\($0)") }
            .navigationDestination(for: Double.self) { Text("Received Double: \\($0)") }
            .navigationTitle("Select a value")
        }
    }
}

NavigationLink 跳转同时执行指令

在 SwiftUI 中,NavigationLink 默认会在点击时立即跳转到目标视图,但如果你希望在点击时执行一些额外的操作(如数据加载、状态更新、日志记录等),可以通过以下几种方式实现:

方法 1: 使用 Button 结合 NavigationLink

你可以在点击按钮时手动控制跳转,同时在点击时执行操作。这可以通过使用 ButtonNavigationLinkisActive 绑定来实现。

import SwiftUI

struct ContentView: View {
    @State private var isNavigationActive = false

    var body: some View {
        VStack {
            Button(action: {
                // 在点击时执行一些操作
                print("执行自定义指令")
                // 触发跳转
                isNavigationActive = true
            }) {
                Text("点击跳转并执行操作")
                    .foregroundColor(.blue)
            }
            // 使用 isActive 绑定控制 NavigationLink
            NavigationLink(destination: DestinationView(), isActive: $isNavigationActive) {
                EmptyView()  // 使用空视图占位,因为跳转通过按钮控制
            }
        }
    }
}

struct DestinationView: View {
    var body: some View {
        Text("目标视图")
    }
}

方法 2: 使用 onTapGestureNavigationLink

如果想继续使用 NavigationLink 的外观而不显式使用按钮,也可以在 NavigationLink 包裹的内容上使用 onTapGesture,执行自定义操作后再进行跳转。

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: DestinationView()) {
                Text("点击跳转并执行操作")
                    .foregroundColor(.blue)
                    .onTapGesture {
                        // 在点击时执行一些操作
                        print("执行自定义指令")
                    }
            }
        }
    }
}

struct DestinationView: View {
    var body: some View {
        Text("目标视图")
    }
}

onTapGesture:使用 onTapGesture 可以在 NavigationLink 的内容上添加点击手势,在跳转前执行自定义指令。虽然 NavigationLink 会在点击时自动跳转,但手势会首先触发并执行操作。

方法 3: 使用 simultaneousGesture

如果你想保留 NavigationLink 的默认点击行为,同时执行额外操作,可以使用 simultaneousGesture,这样不会干扰默认的跳转行为。

import SwiftUI

struct ContentView: View {
    var body: some View {
        NavigationView {
            NavigationLink(destination: DestinationView()) {
                Text("点击跳转并执行操作")
                    .foregroundColor(.blue)
            }
            .simultaneousGesture(TapGesture().onEnded {
                // 在点击时执行一些操作
                print("执行自定义指令")
            })
        }
    }
}

struct DestinationView: View {
    var body: some View {
        Text("目标视图")
    }
}

simultaneousGesture:它允许你在不阻止 NavigationLink 的默认行为的情况下,绑定一个手势来执行自定义操作。在点击 NavigationLink 时,手势会被触发,但跳转行为也会正常发生。