0. 概览
从 SwiftUI 4.0 开始,觉悟了的苹果毅然抛弃了已“药石无效”的 NavigationView,改为使用全新的 NavigationStack 视图。
诚然,NavigationStack 从先进性来说比 NavigationView 有不小的提升,若要如数家珍得单开洋洋洒洒的一篇来介绍。
关于 SwiftUI 中旧 NavigationView 视图种种人神共愤的“诟病”和弊端,请移步我的其它专题博文观赏:
- SwiftUI进入多重嵌套视图后如何一键退回到根视图
- iOS 16.2 在 SwiftUI 子视图中无法关闭弹出的(sheet)导航视图(NavigationView)之解决
- SwiftUI导航至子视图后状态改变导致导航栈提前弹出的原因及解决
- SwiftUI实现不同TabView标签页中任意导航层级视图之间自动相互跳转那些事儿
- SwiftUI中NavigationLink多层嵌套导航无法返回上一层的原因及解决
不过,这些都不是本篇的主旨。在本篇中我们将尝试一起另辟蹊径来完成两种截然不同的导航机制。
在本篇博文,您将学到如下内容:
- 0. 概览
- 1. NavigationStack
- 2. NavigationSplitView 导航之“假象”
- 3. 洞若观火:在 iPad 上的比较
- 4. 总结
无需等待,Let’s go!!!😉
1. NavigationStack
从 SwiftUI 4.0 开始, 引入的新 NavigationStack 导航器终于不再以分散杂乱的数据作为导航触发媒介,而是将有序的数据集合作为导航跳转的核心来对待!(所以,一步跳回根视图成了雕虫小技)
其中,NavigationStack 导航器重要的组成部分 NavigationLink 除了为了兼容性暂时保留的传统跳转方式以外,主打以状态本身的值作为导航的基石。这样,对于不同类型状态触发的跳转,我们可以干净而从容的分别处理:
下面举一例。
首先,定义简单的数据结构,Alliance 中包含若干 Hero:
@Observable
final class Hero {var name: Stringvar power: Intinit(name: String, power: Int) {self.name = nameself.power = power}
}extension Hero: Identifiable {var id: String {name}
}extension Hero: Hashable {static func == (lhs: Hero, rhs: Hero) -> Bool {lhs.name == rhs.name && lhs.power == rhs.power}func hash(into hasher: inout Hasher) {hasher.combine(name)hasher.combine(power)}
}@Observable
final class Alliance: Hashable {static func == (lhs: Alliance, rhs: Alliance) -> Bool {lhs.title == rhs.title}func hash(into hasher: inout Hasher) {hasher.combine(title)}var title: Stringvar createAt: Date?var heros: [Hero]init(title: String, heros: [Hero]) {self.title = titleself.createAt = Date.nowself.heros = heros}
}
接下来是与之对应的子视图,注意驱动 NavigationLink 导航的是 Hero 本身,而不是什么“莫名奇妙”的条件变量:
struct HeroDetailView: View {let hero: Herovar body: some View {VStack {Text("力量: \(hero.power)").font(.largeTitle)}.navigationTitle(hero.name)}
}struct HeroListView: View {@Environment(Alliance.self) var modelvar body: some View { VStack {List(model.heros) { hero inNavigationLink(value: hero) {HStack {Text(hero.name).font(.headline)Spacer()Text("\(hero.power)").font(.subheadline).foregroundStyle(.gray)}}}}}
}
接着是主视图:
struct ContentView: View {@State var model = Alliance(title: "地球超级英雄", heros: [.init(name: "大熊猫侯佩", power: 5),.init(name: "孙悟空", power: 1000),.init(name: "哪吒", power: 511)])var body: some View {NavigationStack {Form {NavigationLink("查看所有英雄", value: model)}.navigationDestination(for: Alliance.self) { model inHeroListView().environment(model)}.navigationDestination(for: Hero.self) { hero inHeroDetailView(hero: hero)}.navigationTitle(model.title)}}
}
从上面源代码中,我们可以看到几处有趣的地方:
- 子视图 HeroListView 和主视图 ContentView 都包含了 NavigationLink,但它们驱动状态的类型不一样(分别是 Hero 和 Model),这样不同的驱动源被清晰的区分开了;
- 放置 NavigationLink 和实际发生导航跳转的目标位置是分开的(通过 navigationDestination() 修改器),后者被放在了一起便于集中管理;
正是这些新的导航特性确保了导航逻辑代码清楚且集中,为日后自己或其它秃头码农来维护打下夯实基础:
以上就是第一种导航方法,即利用 NavigationStack + navigationDestination() 修改器方法来合作完成跳转功能。
2. NavigationSplitView 导航之“假象”
可能有的小伙伴们没太在意,SwiftUI 4.0 除了 NavigationStack 以外还新加入了另一个鲜为人知的导航器 NavigationSplitView!使用它,我们可以抛弃 navigationDestination() 去实现完全相同的导航功能。
我们对之前代码略作修改,看看能促成什么新奇的“玩法”:
struct HeroListView: View {@Environment(Alliance.self) var model@Binding var selection: Hero?var body: some View { VStack {List(model.heros, selection: $selection) { hero inNavigationLink(value: hero) {HStack {Text(hero.name).font(.headline)Spacer()Text("\(hero.power)").font(.subheadline).foregroundStyle(.gray)}}}}}
}struct ContentView: View {@State var model = Alliance(title: "地球超级英雄", heros: [.init(name: "大熊猫侯佩", power: 5),.init(name: "孙悟空", power: 1000),.init(name: "哪吒", power: 511)])@State private var selection: Hero?var body: some View {NavigationSplitView(sidebar: {HeroListView(selection: $selection).environment(model).navigationTitle("新导航方式")}, detail: {if let selection {HeroDetailView(hero: selection)} else {ContentUnavailableView("No Hero", systemImage: "person.badge.key.fill", description: Text("还未选中任何英雄!"))}})}
}
可以看到,修改后的代码与之前有几处不同:
- 使用 NavigationSplitView 而不是 NavigationStack;
- 没有使用任何 navigationDestination() 修改器方法;
- 向 List 构造器传入了 selection 参数,以判断用户选择了哪个 Hero;
- 根据 selection 的值驱动 NavigationSplitView 构造器 detail 闭包完成跳转功能;
简单来说:当用户选中任意 Hero 后,通过设置 NavigationSplitView 构造器 detail 闭包中的视图,我们完成了导航机制。
代码执行结果和之前几乎完全相同,这么神奇!?
可惜,你们看到的全是“假象”!!!
3. 洞若观火:在 iPad 上的比较
其实,设置 NavigationSplitView 构造器 detail 闭包的内容原本并不会造成导航跳转,它的原意是在大屏设备上更方便的利用大尺寸屏幕来浏览内容。
编译上面 NavigationSplitView 的实现,在 iPad 上运行看看效果:
看到了吗?第二种导航机制在大屏设备上原本并不是真正用来导航跳转的,只是在 iPhone 等小屏设备上它的行为退化成了导航!
而第一种导航实现是彻头彻尾、如假包换的“真”导航:
到底哪种方法更好一些?小伙伴们自有“仁者见仁智者见智”的看法,欢迎大家随后的讨论。
至此,我们完成了文章开头的目标,棒棒哒!!!💯
4. 总结
在本篇博文中,我们在 SwiftUI 4.0 里通过两种不同方式实现了相同的子视图导航功能,任君选择。
感谢观赏,再会!😎