使用 NavigationStack 组件进行页面导航
了解如何在 iOS 16 或以上版本中,使用 SwiftUI 最新的 NavigationStack 组件进行页面导航,并学习通过编程的方式控制导航。

NavigationView 是 SwiftUI 的一个组件,从 iOS13 开始被引入,用于实现视图的导航功能。从 iOS16 开始,苹果引入了新的导航组件——NavigationStack,它解决了 NavigationView 组件的一些问题(我们在下面会说明),在处理复杂导航的时候会更加轻松。
在 iOS16 或以上版本中使用 NavigationView,Xcode 会给予该警告:NavigationView will be deprecated in a future version of iOS: use NavigationStack or NavigationSplitView instead.”
,除非你有明确理由必须兼容 iOS 15 或以前版本,否则你都应该使用 NavigationStack。
NavigationStack 与 NavigationView 核心区别
NavigationView 和 NavigationStack 的核心区别之一确实体现在它们的驱动方式上:NavigationView 是基于视图驱动的,而 NavigationStack 现在是基于数据驱动的,这是一个非常受欢迎的改变。
- 视图驱动(例如 NavigationView)是基于界面来导航的。换句话说,点击某个按钮会直接跳转到另一个视图,点击返回也只能回到上一个指定页面,导航的逻辑是围绕“从哪到哪”来设计的,且不容易直接控制导航的过程。
- 数据驱动(例如 NavigationStack)是通过数据来控制导航的。你不仅是在界面上点击按钮跳转视图,还能通过操作导航背后的数据来决定显示哪些视图。例如,你可以直接告诉系统导航到某个页面,不依赖点击具体视图。
比如处理深层次的导航,当用户进入详情页并导航了好几层时(比如进入商品详情,查看评论,查看图片等),你想要通过一个按钮直接回到主页面,视图驱动(NavigationView) 就会比较难处理,而数据驱动(NavigationStack)则能够轻松应对这种场景。
我们通过一个更加具体点的例子来说明这个区别。假设你有一个展示商品的应用,用户可以点击商品进入详情页面。
视图驱动(NavigationView) 的方式是:用户点击商品 -> 进入详情页。这很直观,但如果用户已经在详情页,你想要通过某个事件(比如收到推送通知)直接让用户回到主页面,这就会变得困难。因为视图驱动的导航并没有记录导航路径的具体数据,所以开发者无法轻松操控导航历史。
NavigationView {
List(products) { product in
NavigationLink(destination: ProductDetailView(product: product)) {
Text(product.name)
}
}
}
并且,因为依赖NavigationLink
的点击事件来推送视图。假如你想通过代码在用户点击之前直接打开某个特定的商品详情(例如点击推送消息),操作起来也同样会比较麻烦。
数据驱动(NavigationStack) 的方式是:你可以使用一个路径数组来控制用户在哪个页面。例如,你可以随时通过更新路径数组中的数据,决定当前展示的页面。
@State private var path = [Product]()
NavigationStack(path: $path) {
List(products) { product in
Button(action: {
path.append(product)
}) {
Text(product.name)
}
}
.navigationDestination(for: Product.self) { product in
ProductDetailView(product: product)
}
}
这里,path
是一个数组,它决定了用户导航到哪里。你不仅可以通过点击按钮添加新的视图到导航堆栈,还可以通过其他逻辑(例如推送消息、按钮事件等)随时调整导航路径,比如清空路径让用户回到主页面。
基于视图的简单导航(单一视图目标)
NavigationStack 仍然支持基于视图的简单导航,和 NavigationView 语法类似,这适合一些简单的导航场景。
在这种场景中,只需使用 NavigationStack()
搭配 NavigationLink()
组件即可,无需使用 .navigationDestination()
修饰器。
NavigationLink(_:destination:)
对于仅由文本组成的链接,可以使用一个方便的初始化器,它接受一个字符串并创建一个 Text 视图:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationStack {
VStack(spacing: 20) {
// 导航到详情页面
NavigationLink("查看详情")
{
DetailsView()
}
// 导航到设置页面
NavigationLink("设置")
{
SettingsView()
}
}
.navigationTitle("主页")
.padding()
}
}
}

它的官方文档如下:
NavigationLink(destination:label:)
如果源视图不是 Text,而是 View 视图,则需要使用 NavigationLink, label 闭包来实现:
NavigationLink {
FolderDetail(id: workFolder.id)
} label: {
SourceView()
}
避免使用之前的 NavigationLink(destination)
语法,这会导致导航出现预期之外的错误:
// 该初始化器设计与 NavgationView 搭配使用,不适合 NavigationStack 。
NavigationLink(
destination: DetailView()
) {
SourceView()
}
比如下面这个实例代码:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationStack {
VStack(spacing: 20) {
// 使用自定义视图作为 NavigationLink 的触发器
NavigationLink {
DetailsView()
} label: {
HStack {
Image(systemName: "info.circle")
.font(.title)
Text("查看详情")
.font(.title2)
.bold()
}
.padding()
.background(Color.blue.opacity(0.1))
.cornerRadius(10)
}
// 使用自定义视图导航到设置页面
NavigationLink {
SettingsView()
} label: {
HStack {
Image(systemName: "gearshape")
.font(.title)
Text("设置")
.font(.title2)
.bold()
}
.padding()
.background(Color.green.opacity(0.1))
.cornerRadius(10)
}
}
.navigationTitle("主页")
.padding()
}
}
}

它的官方文档如下:
基于数据的导航(单一视图目标)
对于更加复杂的导航场景,推荐使用基于数据的导航——这也是 NavigationStack 的核心优势之一。
基于数据的导航,核心是使用 .navigationDestination 修饰器(他有多种初始化器方式,适用于不同的场景)。
.navigationDestination(isPresented:destination:)
.navigationDestination(isPresented:)
是 iOS16 中引入的新的修饰符。通过设置一个布尔状态变量,该修饰符可以在满足条件时触发导航,呈现目标视图。
- 以编程方式将单个视图推送到堆栈上。如果是动态的目标视图,例如 List 或者 Stack,应该使用后面讲到的【多视图目标】。
- 这种方式无需使用 NavigationLink 组件。
- 与传统的
.sheet()
用法非常相似,这也是我喜欢使用它的原因之一。
以下示例代码演示了如何在 Button
的点击事件中触发 .navigationDestination
:
import SwiftUI
struct ExampleView: View {
@State private var showDetailView = false
var body: some View {
NavigationStack {
VStack {
Button("Show Detail View") {
showDetailView = true
}
}
.navigationDestination(isPresented: $showDetailView) {
DetailView()
}
.navigationTitle("Main View")
}
}
}
官方文档如下:
基于数据的导航(多视图目标)
要创建基于动态数据的多视图目标的导航,必须使用.navigationDestination(for:destination:)
修饰器,并且需要和 NavigationLink(_:value:)
或 NavigationLink(value:label:)
搭配使用。
也就是说,这种方式必须使用三个组件:
NavigationStack()
—— 一般建议放在 Tabview 下的根视图(若有 Tabview 的话)。.navigationDestination(for:destination:)
NavigationLink(_:value:)
或NavigationLink(value:label:)
另外,Apple 最佳实践推荐使用 enum 来定义和管理导航路径。然后再 NavigationLink(value:) 和 .navigationDestination()中均调用 enum。
.navigationDestination(for:destination:)
这种方式提供了更高的灵活性,使用 navigationDestination(for:destination:)
修饰符将某个数据类型与导航链接关联。可以为同一种数据类型创建多个 NavigationLink
实例,不同的数据值会展示相应的视图。
下面是一个完整实例:
import SwiftUI
// 数据模型:用户
struct User: Identifiable, Hashable {
let id: Int
let name: String
let age: Int
let profileDescription: String
}
// 模拟的用户数据
let users = [
User(
id: 1, name: "Alice", age: 28,
profileDescription: "A software developer from New York."),
User(
id: 2, name: "Bob", age: 32,
profileDescription: "A designer from California."),
User(
id: 3, name: "Charlie", age: 24,
profileDescription: "A student from Texas."),
]
// 主页面
struct ContentView: View {
@State private var navigationPath = NavigationPath()
var body: some View {
NavigationStack(path: $navigationPath) {
VStack(spacing: 40) {
// 用户列表
List(users) { user in
NavigationLink(value: user) {
HStack {
VStack(alignment: .leading) {
Text(user.name)
.font(.headline)
Text("\(user.age) 岁")
.font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
}
.padding(.vertical, 8)
}
}
.listStyle(.insetGrouped)
// 导航到设置页面
NavigationLink("设置", value: "Settings")
.font(.title2)
.padding(.top, 20)
}
.navigationTitle("用户列表")
.navigationDestination(for: User.self) { user in
UserDetailsView(user: user)
}
.navigationDestination(for: String.self) { destination in
if destination == "Settings" {
SettingsView()
}
}
}
}
}
// 用户详情页面
struct UserDetailsView: View {
let user: User
var body: some View {
VStack(spacing: 20) {
Text("姓名: \(user.name)")
.font(.largeTitle)
.bold()
Text("年龄: \(user.age)")
.font(.title3)
.foregroundColor(.secondary)
Text(user.profileDescription)
.font(.body)
.padding()
// 导航到个人资料页面
NavigationLink("查看个人资料") {
UserProfileView(user: user)
}
.padding(.top, 20)
.buttonStyle(.borderedProminent)
}
.navigationTitle("用户详情")
.navigationBarTitleDisplayMode(.inline)
.padding()
}
}
// 用户个人资料页面
struct UserProfileView: View {
let user: User
var body: some View {
VStack(spacing: 20) {
Text("\(user.name) 的个人资料")
.font(.largeTitle)
.bold()
Text("年龄: \(user.age)")
.font(.title3)
.foregroundColor(.secondary)
Text(user.profileDescription)
.font(.body)
.padding()
}
.navigationTitle("个人资料")
.padding()
}
}
// 设置页面
struct SettingsView: View {
@State private var isNotificationsEnabled = true
@State private var isDarkModeEnabled = false
var body: some View {
Form {
Toggle("接收通知", isOn: $isNotificationsEnabled)
Toggle("启用暗黑模式", isOn: $isDarkModeEnabled)
}
.navigationTitle("设置")
}
}
// 预览
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
使用 NavigationPath
进行编程导航
可以通过使用 NavigationPath
来实现编程导航。NavigationPath
是一个集合类型,表示导航栈中的历史记录。通过绑定 NavigationStack
的 path
属性到一个 @State
类型的 NavigationPath
变量,可以跟踪导航状态并实现编程控制。
struct ContentView: View {
let screens = NavigationDestinations.allCases
@State private var navigationPath = NavigationPath()
var body: some View {
NavigationStack(path: $navigationPath) {
VStack(spacing: 40) {
ForEach(screens, id: \.self) { screen in
NavigationLink(value: screen) {
Text(screen.rawValue)
}
}
}
.navigationTitle("Main View")
.navigationDestination(for: NavigationDestinations.self) { screen in
switch screen {
case .Details:
DetailsScreen()
case .Profiles:
ProfileScreen()
case .Settings:
SettingsScreen()
}
}
}
}
}
navigationPath
变量用于跟踪所有的导航历史。当用户导航至某个视图时,NavigationStack
会使用这个绑定状态来管理栈内的导航状态。可以通过直接修改 navigationPath
来实现编程推送和弹出视图。
例如,可以通过以下方式预配置导航路径:
@State private var navigationPath = [NavigationDestinations.Details, NavigationDestinations.Settings, NavigationDestinations.Profiles]
使用 append(_:)
方法在导航栈上推送新的视图,例如:
Button("Add view") {
navigationPath.append(NavigationDestinations.Details)
}
这样,当点击按钮时,Details
视图会被程序化地推送到导航栈中,展示该视图。
自定义导航栏外观[高级]
了解如何使用 SwiftUI 提供的导航相关的修饰器,自定义你的导航栏外观,请继续阅读这篇文章:
相关资源
https://www.swiftanytime.com/blog/navigationstack-in-swiftui