swiftUI中使用NavigationView作为导航视图,长久开发以来受NavigationView能力的限制,导航中的一些功能(返回根视图、堆栈中添加任意视图、返回任意层级视图、Deeplink跳转等)都需要开发者自己来实现,swiftUI4.0中多系统导航栏进行了重大改变,提供了以视图堆栈为管理对象的新的API,开发者可以轻松实现编程式导航。
1.NavigationView缺点
siwftUI4.0以前 常见的tabbar的替代方案是 NavigationView + TabView来实现,由于API的限制和不确定的bug,使用起来就不是很丝滑。
- 角标和红点实现问题
- 无法个性定制NavigationBar,无法添加TabBar
- 无法彻底隐藏NavigationBar,切换Tabbar时不时可能NavigationBar还会出来
- NavigationView在iPad上展示问题,实现tabbar不会全屏展示
- 导航切换问题,无法返回根视图,切换堆栈中任意视图等等
上面的bug
已经有第三方库能够解决,但是总归是望梅止渴,不能根本解决开发者的需求。
2.swiftUI 4.0全新的导航系统
swiftUI 4.0中将NavigationView拆分为NavigationStack
和NavigationSplitView
.
- NavigationStack:针对的是单栏的使用场景,例如iPhone、Apple TV、Apple Watch:
arduino
NavigationStack{}
//相当于
NavigationView{}.navigationViewStyle(.stack)
- NavigationSplitVew:针对多栏场景 例如:iPadOS、masOS
scss
NavigationSplitView{
SideBarView()
} detail: {
DetailView()
}
//对应的双列场景
NavigationView{
SideBarView()
DetailView()
}.navigationViewStyle(.columns)
1.全新导航系统
老版本中使用NavigationView进行导航,大部分是使用的声明式导航
scss
NavigationView{
NavigationView{
List(0..<30,id: \.self){ index in
//设置跳转
NavigationLink(destination: {
DetailPage(message: "我就是展示设置\(index)")
}) {
Text("我就是展示设置\(index)")
}
}
.navigationTitle("我就是我")
//设置标题的样式
.navigationBarTitleDisplayMode(.inline)
}
}
上面使用NavigationLink的方法跳转有一定的局限性
1.需要逐级视图进行绑定,开发者如想实现返回任意层级视图需要自行管理状态
2.声明NavigationLink时要显示设置目标视图,造成不必要的实例创建开销
3.较难从视图外调用导航功能
而NavigationStack从两个角度来解决上面几个问题
1.1 编程式导航-> 处理目标视图
php
enum TargetView {
case subView1, subViwe2
}
NavigationStack{
List{
NavigationLink("SubView1", value: Target.subView1)
NavigationLink("SubView2", value: Target.subView2)
NavigationLink("SubView3", value: 3)
NavigationLink("SubView4", value: 4)
}
//navigationDestination为不同数据 类型添加处理操作
.navigationDestination(for: Target.self) { target in
switch target{
case .subView1:
Text("我是展示数据1")
case .subView2:
Text("我是展示数据2")
}
}.navigationDestination(for: Int.self) { target in
switch target {
case 3:
Text("我是展示数据3")
default:
Text("我是展示数据4")
}
}
}
上述NavigationStack的处理方式有以下特点
- 无需在NavigationLink中立即制定目标视图,避免创建多余的实例视图
- 对同一类型的值定义的Link进行统一管理,利于复杂逻辑判断和代码分离
- NavigationLink将优先使用最接近的类型目标管理代码。例如 根视图和第三层都通过navigationDestination定义了对Int的响应,那么第三层及其之上的视图将使用第三层的处理逻辑
1.2 管理堆栈系统
对比于基于类型的响应式目标视图处理机制,可管理的视图堆栈系统才是新导航系统的杀手锏。 新的导航系统支持两种堆栈管理类型
- 1.NavigationPath:通过添加的多个navigationDestination、NavigationStack可以多种类型值(Hashable)进行相应,使用(removeLast(_ k: Int = 1))返回指定的层级,append进入新的层级
scss
class PathManager: ObservableObject {
@Published var path = NavigationPath()
}
struct NavigationStackTest: View {
@StateObject var pathManager = PathManager()
var body: some View {
NavigationStack(path: $pathManager.path){
List{
NavigationLink("subView1", value: 1)
NavigationLink("subView2", value: TargetView.subView2)
NavigationLink("subView3", value: 3)
NavigationLink("subView4", value: 4)
}
.navigationDestination(for: Target.self) { target in
swift target {
case .subView1:
SubView1()
case .subView2:
Text("我是SubView2")
}
}
.navigationDestination(for: Int.self) { target in
swift target {
case 1:
SubView1()
case 2:
Text("我是SubView2")
case 3:
Text("我是SubView3")
case 4:
Text("我是SubView4")
}
}
.environmnetObject(pathManager)
/// task 函数在组件加载完成之后执行的异步任务,task中会相继执行跳转功能,最后展示subView2界面
.task{
pathManager.path.append(3)
pathManager.path.append(1)
pathManager.path.append(TargetView.subView2)
}
}
}
}
struct SubView1: View {
//获取对应的环境变量 对导航堆栈视图进行处理
@EnvironmentObject var pathManager: PathManager
var body: some View {
List{
// 仍然可以使用此种形式的 NavigationLink,目标视图的处理在根视图对应的 navigationDestination 中
NavigationLink("SubView2", destination: Text("我就是我不一样的烟火") )
NavigationLink("subView3",value: 3)
Button("go to SubView3"){
pathManager.path.append(3) // 效果与上面的 NavigationLink("subView3",value: 3) 一样
}
Button("返回根视图"){
pathManager.path.removeLast(pathManager.path.count)
}
Button("返回上层视图"){
pathManager.path.removeLast()
}
}
}
}
- 2.元素符合Hashable的单一类型序列
若采用此种堆栈,NavigationStack将只能响应该序列元素的特定类型
scss
class PathManager: ObservableObject {
@Published var path: [Int] = [] //Hashable序列
}
struct NavigationStackTest: View {
@StateObject var pathManager = PathManager()
var body: some View {
NavigationStack(path: $pathManager.path){
List{
NavigationLink("subView1", value: 1)
NavigationLink("subView2", value: 2)
NavigationLink("subView3", value: 3)
NavigationLink("subView4", value: 4)
}
.navigationDestination(for: Int.self) { target in
swift target {
case 1:
SubView1()
case 2:
Text("我是SubView2")
case 3:
Text("我是SubView3")
case 4:
Text("我是SubView4")
}
}
.environmnetObject(pathManager)
/// task 函数在组件加载完成之后执行的异步任务,task中会相继执行跳转功能,最后展示subView2界面
.task{
pathManager.path.append(3)
pathManager.path.append(1)
pathManager.path.append(4)
}
}
}
}
struct SubView1: View {
//获取对应的环境变量 对导航堆栈视图进行处理
@EnvironmentObject var pathManager: PathManager
var body: some View {
List{
// 仍然可以使用此种形式的 NavigationLink,目标视图的处理在根视图对应的 navigationDestination 中
NavigationLink("SubView2", destination: Text("我就是我不一样的烟火") )
NavigationLink("subView3",value: 3)
// 效果与上面的 NavigationLink("subView3",value: 3) 一样
Button("go to SubView3"){
pathManager.path.append(3)
}
Button("返回根视图"){
pathManager.path.removeLast(pathManager.path.count)
}
Button("返回上层视图"){
pathManager.path.removeLast()
}
// 会自动屏蔽动画
Button("响应 Deep Link,重置 Path Stack "){
pathManager.path = [3,2,1]
}
}
}
}
上面两种堆栈视图处理方式开发者可根据需要自行选择。
使用堆栈管理视图时,不要在编程式的导航中混用声明式的导航,会破坏当前的视图堆栈数据
less
/// 编程式导航
NavigationLink("SubView3",value: 3)
// 声明式导航
NavigationLink("SubView4", destination: { SubView4() })
2.NavigationSplitView
NavigationSplitView是用于多列不同栏数据直接进行展示的。
上面展示的是在ipad进行的一个三栏的展示视图,选中第一栏和第二栏,右侧的第三栏都会跟着联动
less
class MyStore: ObservableObject {
//展示当前第一栏选中
@Published var row: Int?
//第二栏选中
@Published var selection: Int?
}
struct ZJSwiftUI_NavigationSplitView: View {
@StateObject var store = MyStore()
/// 设置NavigationSplitView展示样式
@State var mode: NavigationSplitViewVisibility = .all
var body: some View {
_testSlideBarThree()
}
/// navigationSplitViewColumnWidth 设置分栏的宽度
func _testSlideBarThree() -> some View {
NavigationSplitView(columnVisibility: $mode) {
SideBarView()
.navigationSplitViewColumnWidth(150)
} content: {
SideContentView()
.navigationSplitViewColumnWidth(min: 200, ideal: 250, max: 300)
} detail: {
DetailView()
.toolbar{
EditButton() //在detail栏中NavigationView 创建按钮
}
.navigationTitle("Detail")
}.environmentObject(store)
}
}
struct SideBarView: View {
@EnvironmentObject var store: MyStore
var body: some View {
List(0..<30, id: \.self){ i in
//修改selection的值,让右侧视图响应该值的变化
Button("ID:\(i)"){
//选中第一个 同时更新中间和右侧的边栏
store.row = i
store.selection = 0;
}
.padding(5)
.foregroundStyle(store.row == i ? .white : .black)
.background(store.row == i ? .blue : .clear)
}
}
}
struct SideContentView: View {
@EnvironmentObject var store: MyStore
var body: some View {
NavigationStack{
List(0..<30, id: \.self){ i in
//修改selection的值,让右侧视图响应该值的变化
if let row = store.row {
Button("已选中:\(row),ContentID:\(i)"){
store.selection = i
}
.padding(5)
.foregroundStyle(store.selection == i ? .white : .black)
.background(store.selection == i ? .blue : .clear)
}else{
Button("未选中 ContentID:\(i)"){
store.selection = i
}
}
}
}
.toolbar(.hidden, for: .navigationBar)
.navigationBarTitleDisplayMode(.inline)
}
}
struct DetailView: View {
@EnvironmentObject var store: MyStore
/// 对分栏中的详情栏 也嵌入一个可以实现跳转的NavigationView 会有很大问题,此时Detail栏中将会出现两个NavigationTitle以及两个ToolBal
/// 儿子
var body: some View {
NavigationStack{
VStack{
if let selection = store.selection {
Text("视图:\(selection)")
}else{
Text("请选择")
}
}.navigationDestination(for: Int.self) {
Text("详情界面跳转==\($0)")
}
.toolbar {
RenameButton()
}
.navigationTitle("Detail inlie")
.navigationBarTitleDisplayMode(.inline)
}
}
}
上面是三栏导航的简单实现,除了使用button进行点击处理,也可以在ListView中可以直接绑定数据,无需通过 button显示进行修改,使用NavigationLink进行编程式跳转。
php
List(0..<30, id:\.self, selection: $store.row){ i in
NavigationLink("ID: \(i)", value: i)
}
而在swiftUI4.0中系统为List提供了一种更便利的方式,可以不使用NavigationLink,直接使用下面的方式
swift
struct SideBarView: View {
@EnvironmentObject var store: MyStore
var body: some View {
List(0..<30, id: \.self, selection: $store.selection) { i in
// 也可以换成 Label 或其他视图 ,但不能是 Button
Text("ID: \(i)")
}
}
}
在list进行数据绑定之后,通过list的构造方法创建的循环或forEach创建的循环中的内容(不能自带点击属性,例如button或onTapGesture),将被隐式添加tag修饰符,从而具备点击后更改绑定数据的能力。
2.1 NavigationSplitView视图功能修改
1.设置分栏展示的宽度
navigationSplitViewColumnWidth设置固定或者根据上下文进行一个适配的宽度
scss
NavigationSplitView(columnVisibility: $mode) {
SideBarView()
.navigationSplitViewColumnWidth(150)
} content: {
SideContentView()
.navigationSplitViewColumnWidth(min: 200, ideal: 250, max: 300)
} detail: {
DetailView()
.toolbar{
EditButton() //在detail栏中NavigationView 创建按钮
}
.navigationTitle("Detail")
}.environmentObject(store)
2.在第二分栏和第三分栏设置ToolBar按钮
使用NavigationStack在第二栏或第三栏中 进行组合使用,可以对其NavigationBarTitle进行设置,详情见上面代码,如果直接使用NavigationView,detail栏中将出现两个NavigationTitle以及两个ToolBar,该场景可以自己进行复现,此处不在赘述。
3.动态控制多栏显示状态
NavigationSplitView
构造时可以通过ColumnVisibility来动态的控制多栏的显示状态。
scss
struct NavigationSplitViewDemo: View {
@State var mode: NavigationSplitViewVisibility = .all
var body: some View {
NavigationSplitView(columnVisibility: $mode) {
SideBarView()
}
content: {
SideContentView()
}
detail: {
DetailView()
}
.navigationSplitViewStyle(.balanced) // 设置样式
}
}
- detailOnly 只显示 Detail 栏( 最右侧栏 )
- doubleColumn 在三栏状态下隐藏 Sidebar ( 最左侧 )栏
- all 显示所有的栏
- automatic 根据当前的上下文自动决定显示行为
4.设置NavigationSplitView样式
scss
struct NavigationSplitViewDemo: View {
var body: some View {
NavigationSplitView {
SideBarView()
}
content: {
SideContentView()
}
detail: {
DetailView()
}
.navigationSplitViewStyle(.balanced) // 设置样式
}
}
navigationSplitViewStyle
几种样式如下
-
prominentDetail 无论左侧栏显示与否,保持右侧的 Detail 栏尺寸不变( 通常是全屏 )。iPad 在 Portrait 显示状态下,默认即为此种模式
-
balanced 在显示左侧栏的时候,缩小右侧 Detail 栏的尺寸。iPad 在 landscape 显示状态下,默认即为此种模式
-
automatic 默认值,根据上下文自动调整外观样式
5.NavigationTitle中设置菜单
使用新的NavigationTitle构造方法,可以将菜单嵌套在标题栏中
6.更改NavigationBar背景色
less
NavigationStack{
List(0..<30, id: \.self){ i in
Button("未选中 ContentID:\(i)"){
store.selection = i
}
}
.toolbarBackground(.pink, for: .navigationBar)
}
设置navigationBar的背景色,只有在List进行滚动的时背景才会变色。
7.隐藏Toolbar
less
NavigationStack{
List(0..<30, id: \.self){ i in
Button("未选中 ContentID:\(i)"){
store.selection = i
}
}
.toolbar(.hidden, for: .navigationBar)
}
8.设置Toolbar角色设置
swift
struct ToolRoleTest: View {
var body: some View {
NavigationStack {
List(0..<10, id: \.self) {
NavigationLink("ID: \($0)", value: $0)
}
.navigationDestination(for: Int.self) {
Text("\($0)")
.navigationTitle("Title for \($0)")
.toolbarRole(.editor)
}
.toolbarRole(.browser)
}
}
}
- navigationStack 默认角色 长按可显示视图堆栈列表
- browser 在iPad下 当前视图的Title将显示在左侧
- editor 不显示返回按钮旁边的上页视图title