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(_:) 这样的导航分栏视图样式修饰符。