SwiftUI 4.0 的全新导航系统
SwiftUI 4.0 的全新导航系统
长久以来,开发者对 SwiftUI 的导航系统颇有微词。受 NavigationView 的能力限制,开发者需要动用各种技巧乃至黑科技才能实现一些本应具备基本功能(例如:返回根视图、向堆栈添加任意视图、返回任意层级视图 、Deep Link 跳转等 )。SwiftUI 4.0( iOS 16+ 、macOS 13+ )对导航系统作出了重大改变,提供了以视图堆栈为管理对象的新 API ,让开发者可以轻松实现编程式导航。本文将对新的导航系统作以介绍。
原文发表在我的博客 肘子的 Swift 记事本 。 由于技术文章需要不断的迭代,当前耗费了不少的精力在不同的平台之间来维持文章的更新。故从 2024 年起,新的文章将只发布在我的博客上。
一分为二
新的导航系统最直接的变化是废弃了 NavigationView,将其功能分成了两个单独的控件 NavigationStack 和 NavigationSplitView。
NavigationStack 针对的是单栏的使用场景,例如 iPhone 、Apple TV、Apple Watch:
NavigationStack {}
// 相当于
NavigationView{}
.navigationViewStyle(.stack)
NavigationSplitView 则针对的是多栏场景,例如 :iPadOS 、macOS:
NavigationSplitView {
SideBarView()
} detail: {
DetailView()
}
// 对应的是双列场景
NavigationView {
SideBarView()
DetailView()
}
.navigationViewStyle(.columns)
NavigationSplitView {
SideBarView()
} content: {
ContentView()
} detail: {
DetailView
}
// 对应的是三列场景
NavigationView {
SideBarView()
ContentView()
DetailView()
}
.navigationViewStyle(.columns)
相较于通过 navigationViewStyle 设定 NavigationView 样式的做法,一分为二的方式将让布局表达更加清晰,同时也会强迫开发者为 SwiftUI 应用对 iPadOS 和 macOS 做更多的适配。
在 iPhone 这类设备中,NavigationSplitView 会自动进行单栏适配。但是无论是切换动画、编程式 API 接口等多方面都与 NavigationStack 明显不同。因此对于支持多硬件平台的应用来说,最好针对不同的场景分别使用对应的导航控件。
两个组件两种逻辑
相较于控件名称上的改变,编程式导航 API 才是本次更新的最大亮点。使用新的编程式 API ,开发者可以轻松地实现例如:返回根视图、在当前视图堆栈中添加任意视图( 视图跳转 )、视图外跳转( Deep Link )等功能。
苹果为 NavigationStack 和 NavigationSplitView 提供了两种不同逻辑的 API ,这点或许会给部分开发者造成困扰。
NavigationView 的编程式导航
NavigationView 其实是具备一定的编程式导航能力的,比如,我们可以通过以下两种 NavigationLink 的构造方法来实现有限的编程式跳转:
init<S>(_ title: S, isActive: Binding<Bool>, @ViewBuilder destination: () -> Destination)
init<S, V>(_ title: S, tag: V, selection: Binding<V?>, @ViewBuilder destination: () -> Destination)
上述两种方法有一定的局限性:
- 需要逐级视图进行绑定,开发者如想实现返回任意层级视图则需要自行管理状态
- 在声明 NavigationLink 时仍需设定目标视图,会造成不必要的实例创建开销
- 较难实现从视图外调用导航功能
“能用,但不好用” 可能就是对老版本编程式导航比较贴切地总结。
NavigationStack
NavigationStack 从两个角度入手以解决上述问题。
基于类型的响应式目标视图处理机制
比如下面的代码是在老版本( 4.0 之前 )SwiftUI 中使用编程式跳转的一种方式:
struct NavigationViewDemo: View {
@State var selectedTarget: Target?
@State var target: Int?
var body: some View {
NavigationView{
List{
NavigationLink("SubView1", destination: SubView1(), tag: Target.subView1, selection: $selectedTarget) // SwiftUI 在进入当前视图时,无论是否进入目标视图,均将创建其实例( 不对 body 求值 )
NavigationLink("SubView2", destination: SubView2(), tag: Target.subView1, selection: $selectedTarget)
NavigationLink("SubView3", destination: SubView3(), tag: 3, selection: $target)
NavigationLink("SubView4", destination: SubView4(), tag: 4, selection: $target)
}
}
}
enum Target {
case subView1,subView2
}
}
NavigationStack 实现上述功能将更加地清晰、灵活和高效。
struct NavigationStackDemo: View {
var body: some View {
NavigationStack {
List {
NavigationLink("SubView1", value: Target.subView1) // 只声明关联的状态值
NavigationLink("SubView2", value: Target.subView2)
NavigationLink("SubView3", value: 3)
NavigationLink("SubView4", value: 4)
}
.navigationDestination(for: Target.self){ target in // 对同一类型进行统一处理,返回目标视图
switch target {
case .subView1:
SubView1()
case .subView2:
SubView2()
}
}
.navigationDestination(for: Int.self) { target in // 为不同的类型添加多个处理模块
switch target {
case 3:
SubView3()
default:
SubView4()
}
}
}
}
enum Target {
case subView1,subView2
}
}
NavigationStack 的处理方式有以下特点和优势:
- 由于无需在 NavigationLink 中指定目标视图,因此无须创建多余的视图实例
- 对由同一类型的值驱动的目标进行统一管理( 可以将堆栈中所有视图的 NavigationLink 处理程序统一到根视图中 ),有利于复杂的逻辑判断,也方便剥离代码
- NavigationLink 将优先使用最接近的类型目标管理代码。例如根视图,与第三层视图都通过 navigationDestination 定义了对 Int 的响应,那么第三层及其之上的视图将使用第三层的处理逻辑
可管理的视图堆栈系统
相较于基于类型的响应式目标视图处理机制,可管理的视图堆栈系统才是新导航系统的杀手锏。
NavigationStack 支持两种堆栈管理类型:
- NavigationPath
通过添加多个的 navigationDestination ,NavigationStack 可以对多种类型值( Hashable )进行响应,使用removeLast(_ k: Int = 1)
返回指定的层级,使用append
进入新的层级
class PathManager:ObservableObject{
@Published var path = NavigationPath()
}
struct NavigationViewDemo1: View {
@StateObject var pathManager = PathManager()
var body: some View {
NavigationStack(path:$pathManager.path) {
List {
NavigationLink("SubView1", value: 1)
NavigationLink("SubView2", value: Target.subView2)
NavigationLink("SubView3", value: 3)
NavigationLink("SubView4", value: 4)
}
.navigationDestination(for: Target.self) { target in
switch target {
case .subView1:
SubView1()
case .subView2:
SubView2()
}
}
.navigationDestination(for: Int.self) { target in
switch target {
case 1:
SubView1()
case 3:
SubView3()
default:
SubView4()
}
}
}
.environmentObject(pathManager)
.task{
// 使用 append 可以跳入指定层级,下面将为 root -> SubView3 -> SubView1 -> SubView2 ,在初始状态添加层级将屏蔽动画
pathManager.path.append(3)
pathManager.path.append(1)
pathManager.path.append(Target.subView2)
}
}
}
enum Target {
case subView1, subView2
}
struct SubView1: View {
@EnvironmentObject var pathManager:PathManager
var body: some View {
List{
// 仍然可以使用此种形式的 NavigationLink,目标视图的处理在根视图对应的 navigationDestination 中
NavigationLink("SubView2", destination: Target.subView2 )
NavigationLink("subView3",value: 3)
Button("go to SubView3"){
pathManager.path.append(3) // 效果与上面的 NavigationLink("subView3",value: 3) 一样
}