• SwiftUI 4.0:两种方式实现子视图导航功能


    在这里插入图片描述

    0. 概览

    从 SwiftUI 4.0 开始,觉悟了的苹果毅然抛弃了已“药石无效”的 NavigationView,改为使用全新的 NavigationStack 视图。

    诚然,NavigationStack 从先进性来说比 NavigationView 有不小的提升,若要如数家珍得单开洋洋洒洒的一篇来介绍。


    关于 SwiftUI 中旧 NavigationView 视图种种人神共愤的“诟病”和弊端,请移步我的其它专题博文观赏:


    不过,这些都不是本篇的主旨。在本篇中我们将尝试一起另辟蹊径来完成两种截然不同的导航机制。

    无需等待,Let’s go!!!😉


    1. NavigationStack

    从 SwiftUI 4.0 开始, 引入的新 NavigationStack 导航器终于不再以分散杂乱的数据作为导航触发媒介,而是将有序的数据集合作为导航跳转的核心来对待!(所以,一步跳回根视图成了雕虫小技)

    其中,NavigationStack 导航器重要的组成部分 NavigationLink 除了为了兼容性暂时保留的传统跳转方式以外,主打以状态本身的值作为导航的基石。这样,对于不同类型状态触发的跳转,我们可以干净而从容的分别处理:

    下面举一例。

    首先,定义简单的数据结构,Alliance 中包含若干 Hero:

    @Observable
    final class Hero {
        var name: String
        var power: Int
        
        init(name: String, power: Int) {
            self.name = name
            self.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: String
        var createAt: Date?
        var heros: [Hero]
        
        init(title: String, heros: [Hero]) {
            self.title = title
            self.createAt = Date.now
            self.heros = heros
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    接下来是与之对应的子视图,注意驱动 NavigationLink 导航的是 Hero 本身,而不是什么“莫名奇妙”的条件变量:

    struct HeroDetailView: View {
        let hero: Hero
        
        var body: some View {
            VStack {
                Text("力量: \(hero.power)")
                    .font(.largeTitle)
                
            }.navigationTitle(hero.name)
        }
    }
    
    struct HeroListView: View {
        @Environment(Alliance.self) var model
        
        var body: some View {        
            VStack {
                List(model.heros) { hero in
                    NavigationLink(value: hero) {
                        HStack {
                            Text(hero.name)
                                .font(.headline)
                            Spacer()
                            Text("\(hero.power)")
                                .font(.subheadline)
                                .foregroundStyle(.gray)
                        }
                    }
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    接着是主视图:

    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 in
                    HeroListView()
                        .environment(model)
                }
                .navigationDestination(for: Hero.self) { hero in
                    HeroDetailView(hero: hero)
                }
                .navigationTitle(model.title)
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    从上面源代码中,我们可以看到几处有趣的地方:

    1. 子视图 HeroListView 和主视图 ContentView 都包含了 NavigationLink,但它们驱动状态的类型不一样(分别是 Hero 和 Model),这样不同的驱动源被清晰的区分开了;
    2. 放置 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 in
                    NavigationLink(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("还未选中任何英雄!"))
    
                }
            })
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    可以看到,修改后的代码与之前有几处不同:

    1. 使用 NavigationSplitView 而不是 NavigationStack;
    2. 没有使用任何 navigationDestination() 修改器方法;
    3. 向 List 构造器传入了 selection 参数,以判断用户选择了哪个 Hero;
    4. 根据 selection 的值驱动 NavigationSplitView 构造器 detail 闭包完成跳转功能;

    简单来说:当用户选中任意 Hero 后,通过设置 NavigationSplitView 构造器 detail 闭包中的视图,我们完成了导航机制。

    代码执行结果和之前几乎完全相同,这么神奇!?

    可惜,你们看到的全是“假象”!!!

    3. 洞若观火:在 iPad 上的比较

    其实,设置 NavigationSplitView 构造器 detail 闭包的内容原本并不会造成导航跳转,它的原意是在大屏设备上更方便的利用大尺寸屏幕来浏览内容。

    编译上面 NavigationSplitView 的实现,在 iPad 上运行看看效果:

    在这里插入图片描述

    看到了吗?第二种导航机制在大屏设备上原本并不是真正用来导航跳转的,只是在 iPhone 等小屏设备上它的行为退化成了导航!

    而第一种导航实现是彻头彻尾、如假包换的“真”导航:

    在这里插入图片描述

    到底哪种方法更好一些?小伙伴们自有“仁者见仁智者见智”的看法,欢迎大家随后的讨论。

    至此,我们完成了文章开头的目标,棒棒哒!!!💯

    4. 总结

    在本篇博文中,我们在 SwiftUI 4.0 里通过两种不同方式实现了相同的子视图导航功能,任君选择。

    感谢观赏,再会!😎

  • 相关阅读:
    java Spring Boot在配置文件中关闭热部署
    Webhook 是什么?Webhook与API有什么区别
    elasticsearch查询之keyword字段的查询相关度评分控制
    HTML基础笔记
    Hive基础(DML 数据操作)
    一款好用的内存清理工具
    笔记整理(安全)
    jQuery事件与特效
    修改了Android Studio 中的这两个面板配置后,代码写的更舒服了~
    数据结构(递归,链表实现递归)
  • 原文地址:https://blog.csdn.net/mydo/article/details/133552939