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低版本导航

相关推荐
夏河始溢1 分钟前
一七八、Node.js PM2使用介绍
前端·javascript·node.js·pm2
记忆深处的声音2 分钟前
vue2 + Element-ui 二次封装 Table 组件,打造通用业务表格
前端·vue.js·代码规范
陈随易3 分钟前
兔小巢收费引发的论坛调研Node和Deno有感
前端·后端·程序员
熊的猫17 分钟前
webpack 核心模块 — loader & plugins
前端·javascript·chrome·webpack·前端框架·node.js·ecmascript
速盾cdn24 分钟前
速盾:vue的cdn是干嘛的?
服务器·前端·网络
四喜花露水1 小时前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
前端Hardy1 小时前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
chaosama1 小时前
禁止uni小程序ios端上下拉伸(橡皮筋效果)
ios·小程序
Zender Han2 小时前
Flutter自定义矩形进度条实现详解
android·flutter·ios
web Rookie2 小时前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript