SwiftUI之全新的导航系统

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拆分为NavigationStackNavigationSplitView.

  • 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修饰符,从而具备点击后更改绑定数据的能力。

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

3.参考文献

SwiftUI 4.0 的全新导航系统

NavigationBackport低版本导航

相关推荐
passerby606119 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了19 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅19 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅20 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
未来侦察班20 小时前
一晃13年过去了,苹果的Airdrop依然很坚挺。
macos·ios·苹果vision pro
崔庆才丨静觅20 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment20 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅21 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊21 小时前
jwt介绍
前端
爱敲代码的小鱼21 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax