6.3 迁移到新的导航类型
原文链接: https://developer.apple.com/documentation/swiftui/migrating-to-new-navigation-types
6.3 迁移到新的导航类型
用导航栈和导航分栏视图替换导航视图,改善应用的导航行为。
概述
如果你的应用最低部署目标是 iOS 16、iPadOS 16、macOS 13、tvOS 16、watchOS 9、visionOS 1 或更高版本,请停止使用 NavigationView,改用 NavigationStack 和 NavigationSplitView 实例。具体怎么用,取决于你是在单栏中导航还是跨多栏导航。使用这些更新的容器,你可以更好地控制视图呈现、容器配置和编程式导航。
更新单栏导航
如果你的应用使用 NavigationView、并以 stack 导航视图样式来设置(人们通过把新视图推入栈来导航),请改用 NavigationStack。
具体来说,不要再这样写:
1
2
3
4
| NavigationView { // 这种写法已弃用。
/* 内容 */
}
.navigationViewStyle(.stack)
|
取而代之,创建一个导航栈:
1
2
3
| NavigationStack {
/* 内容 */
}
|
更新多栏导航
如果你的应用使用两栏或三栏的 NavigationView,或者应用在某些情况下有多栏、在另一些情况下只有单栏(在同时运行于 iPhone 和 iPad 的应用中很常见),请改用 NavigationSplitView。
不要再使用两栏导航视图:
1
2
3
4
| NavigationView { // 这种写法已弃用。
/* 第 1 栏 */
/* 第 2 栏 */
}
|
而是用 init(sidebar:detail:) 构造器,创建一个明确包含侧边栏和详情内容的导航分栏视图:
1
2
3
4
5
| NavigationSplitView {
/* 第 1 栏 */
} detail: {
/* 第 2 栏 */
}
|
类似地,不要再使用三栏导航视图:
1
2
3
4
5
| NavigationView { // 这种写法已弃用。
/* 第 1 栏 */
/* 第 2 栏 */
/* 第 3 栏 */
}
|
而是用 init(sidebar:content:detail:) 构造器,创建一个明确包含侧边栏、内容和详情三部分的导航分栏视图:
1
2
3
4
5
6
7
| NavigationSplitView {
/* 第 1 栏 */
} content: {
/* 第 2 栏 */
} detail: {
/* 第 3 栏 */
}
|
如果你需要在某一栏内部导航,可以在该栏中嵌入一个导航栈。这种安排让你能更精细地控制每一栏显示什么。NavigationSplitView 还让你可以自定义各栏的可见性和宽度。
更新编程式导航
如果你使用带 isActive 输入参数的 NavigationLink 构造器进行编程式导航,请把自动化逻辑移到外层的栈上。做法是把导航链接改用 init(value:label:) 构造器,然后使用接收路径输入的某个导航栈构造器,例如 init(path:root:)。
例如,如果你有一个导航视图,其中的链接响应各自的状态变量来激活:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| @State private var isShowingPurple = false
@State private var isShowingPink = false
@State private var isShowingOrange = false
var body: some View {
NavigationView { // 这种写法已弃用。
List {
NavigationLink("Purple", isActive: $isShowingPurple) {
ColorDetail(color: .purple)
}
NavigationLink("Pink", isActive: $isShowingPink) {
ColorDetail(color: .pink)
}
NavigationLink("Orange", isActive: $isShowingOrange) {
ColorDetail(color: .orange)
}
}
}
.navigationViewStyle(.stack)
}
|
当代码的其他部分把某个状态变量设为 true 时,标签与之匹配的那个导航链接就会激活。
把这个改写成接收路径输入的导航栈:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| @State private var path: [Color] = [] // 默认栈上没有任何内容。
var body: some View {
NavigationStack(path: $path) {
List {
NavigationLink("Purple", value: .purple)
NavigationLink("Pink", value: .pink)
NavigationLink("Orange", value: .orange)
}
.navigationDestination(for: Color.self) { color in
ColorDetail(color: color)
}
}
}
|
这个版本使用 navigationDestination(for:destination:) 视图修饰符,把所呈现的数据与对应的视图解耦。这样 path 数组就能表示栈上的每一个视图。你对数组所做的改动,会同时影响容器当下显示的内容,以及人们在栈中导航时会遇到的内容。如果你用 NavigationPath 而不是普通的数据集合来存储路径信息,还可以支持更复杂的编程式导航。更多信息参见 NavigationStack。
更新基于选择的导航
如果你在 List 元素上使用带 selection 输入参数的某个 NavigationLink 构造器进行编程式导航,可以把选择移到列表上。例如,假设你有一个导航视图,其中的链接响应 selection 状态变量来激活:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| let colors: [Color] = [.purple, .pink, .orange]
@State private var selection: Color? = nil // 默认没有任何选择。
var body: some View {
NavigationView { // 这种写法已弃用。
List {
ForEach(colors, id: \.self) { color in
NavigationLink(color.description, tag: color, selection: $selection) {
ColorDetail(color: color)
}
}
}
Text("Pick a color")
}
}
|
使用同样的属性,你可以把 body 改写成:
1
2
3
4
5
6
7
8
9
10
11
12
13
| var body: some View {
NavigationSplitView {
List(colors, id: \.self, selection: $selection) { color in
NavigationLink(color.description, value: color)
}
} detail: {
if let color = selection {
ColorDetail(color: color)
} else {
Text("Pick a color")
}
}
}
|
列表会与导航逻辑协同:在代码的其他部分改变选择状态变量,会激活对应颜色的导航链接。类似地,如果有人选择了与某个颜色关联的导航链接,列表会更新选择值,供代码的其他部分读取。
用可用性检查提供向后兼容
如果你的应用需要运行在低于 iOS 16、iPadOS 16、macOS 13、tvOS 16、watchOS 9、visionOS 1 的平台版本上,你可以用可用性条件在继续支持旧客户端的同时开始迁移。例如,你可以创建一个自定义包装视图,按条件使用 NavigationSplitView 或 NavigationView:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| struct NavigationSplitViewWrapper<Sidebar, Content, Detail>: View
where Sidebar: View, Content: View, Detail: View
{
private var sidebar: Sidebar
private var content: Content
private var detail: Detail
init(
@ViewBuilder sidebar: () -> Sidebar,
@ViewBuilder content: () -> Content,
@ViewBuilder detail: () -> Detail
) {
self.sidebar = sidebar()
self.content = content()
self.detail = detail()
}
var body: some View {
if #available(iOS 16, macOS 13, tvOS 16, watchOS 9, visionOS 1, *) {
// 使用最新的 API。
NavigationSplitView {
sidebar
} content: {
content
} detail: {
detail
}
} else {
// 支持更早的平台版本。
NavigationView {
sidebar
content
detail
}
.navigationViewStyle(.columns)
}
}
}
|
你可以按应用的需要定制这个包装视图。例如,你可以在可用性检查的相应分支里,给 NavigationSplitView 添加 navigationSplitViewStyle(_:) 这样的导航分栏视图样式修饰符。