通过枚举优化NavigationStack的路径绑定

SwiftUI中的NavigationStack是iOS 16引入基于数据类型驱动的导航视图。通过将数据类型与视图关联起来,提供了类型安全的使用方式。本文并非介绍NavigationStack基本使用的文章,故这里就不多做介绍了。

在实际应用中,通过编码的方式维护导航视图栈是非常常见的行为,比如:打开不同的视图;退出部分视图等。虽然在官方的NavigationStack示例中,也提供了维护导航视图路径的方法,但仅能支持一些简单的场景。另外,在使用上也存在一些局限性。整体的可操作性与UIKit中的UINavigationController有些差距。

下面先来看看示例文档中路径绑定的例子。

NavigationStack的路径绑定方法

当需要维护多个视图界面时,通过在初始化方法中传入绑定path的数组,然后通过对数组的操作来完成视图的导航,比如:给数组插入元素或者删除元素,即完成界面的打开和退出。

swift 复制代码
@State private var presentedParks: [Park] = []

NavigationStack(path: $presentedParks) {
    List(parks) { park in
        NavigationLink(park.name, value: park)
    }
    .navigationDestination(for: Park.self) { park in
        ParkDetails(park: park)
    }
}

这个示例中的问题是只能支持打开相同的界面,因为绑定的数组容器元素是一个具体的类型。

为了解决这个问题,需要使用到类型无关的列表容器NavigationPath,初始化path参数也支持传入绑定NavigationPath。

swift 复制代码
@State private var presentedPaths = NavigationPath()

NavigationStack(path: $presentedPaths) {
    List {
        ForEach(parks) { park in
            NavigationLink(park.name, value: park)
        }
        ForEach(zoos) { zoo in
            NavigationLink(zoo.name, value: zoo)
        }
    }
    .navigationDestination(for: Park.self) { park in
        ParkDetails(park: park)
    }
    .navigationDestination(for: Zoo.self) { zoo in
        ZooDetails(park: zoo)
    }
}

NavigationPath也支持简单的数据添加和删除元素的等操作。

swift 复制代码
/// Appends a new value to the end of this path.
public mutating func append<V>(_ value: V) where V : Hashable

/// Appends a new codable value to the end of this path.
public mutating func append<V>(_ value: V) where V : Decodable, V : Encodable, V : Hashable

/// Removes values from the end of this path.
///
/// - Parameters:
///   - k: The number of values to remove. The default value is `1`.
///
/// - Precondition: The input parameter `k` must be greater than or equal
///   to zero, and must be less than or equal to the number of elements in
///   the path.
public mutating func removeLast(_ k: Int = 1)

日常使用中的问题

虽然上述2种方法提供了维护视图栈的方法,但在实际使用过程中还是会有一些小问题:

  1. 总是需要创建一个类型来关联视图。比如:某些静态界面或者本来就不需要传入参数的视图。
  2. 类型与单个视图绑定。比如:多个视图接收相同的模型参数。
  3. 在视图中无法直接获取绑定的导航数据列表容器。
  4. NavigationPath中提供的容器操纵方法不够。

基于枚举的路径绑定

Swift中的枚举非常强大,跟Objective-C和C中的完全不是一种东西。甚至可以说如果不会使用枚举,就根本不了解Swift。这篇文章非常建议阅读:Swift 最佳实践之 Enum

首先,我们通过枚举来表示应用中的视图类型,结合嵌套的枚举来表示子级的视图类型。另外,通过关联值来传递子视图或视图的入参。

swift 复制代码
enum AppViewRouter: Hashable {
    
    enum Category: Hashable {
        case list
        case detail(Int)
    }
    
    case profile
    case category(Category)
    
    var title: String {
        switch self {
        case .profile:
            return "Profile"
        case .category(let category):
            switch category {
            case .list:
                return "Category List"
            case .detail(let id):
                return "Category Detail: \(id)"
            }
        }
    }
}

在NavigationStack的初始化方法中,通过包含视图枚举的数组来进行绑定。在一个navigationDestination中通过不同的枚举来完成对不同的视图创建,通过编译器,也可以帮助我们不会遗漏没有处理的视图。

swift 复制代码
struct ContentView: View {
    
    @State
    private var presentedRouters: [AppViewRouter] = []
    
    var body: some View {
        NavigationStack(path: $presentedRouters) {
            LinkView(title: "Home")
                .navigationBarTitleDisplayMode(.inline)
                .navigationDestination(for: AppViewRouter.self) { park in
                    switch park {
                    case .profile:
                        LinkView(title: park.title)
                    case .category(let category):
                        switch category {
                        case .list:
                            LinkView(title: park.title)
                        case .detail(let id):
                            LinkView(title: park.title)
                        }
                    }
                }
        }
        .environment(\.myNavigationPath, $presentedRouters)
    }
}

为了能够在子视图中操作当前的导航栈,我们创建了一个环境Binding值,将当前的绑定的枚举数组注入到环境中。

swift 复制代码
private struct MyNavigationPathKey: EnvironmentKey {
    static let defaultValue: Binding<[AppViewRouter]> = .constant([AppViewRouter]())
}

extension EnvironmentValues {
    var myNavigationPath: Binding<[AppViewRouter]> {
        get { self[MyNavigationPathKey.self] }
        set { self[MyNavigationPathKey.self] = newValue }
    }
}

在LinkView中,我们获取绑定路径的环境Binding值,通过对路径数据的添加或删除操作,以实现导航栈的控制。当然,也可以使用NavigationLink继续打开新的视图,以一种类型安全并且带有层级结构的方式。

swift 复制代码
struct LinkView: View {
    
    let title: String
    
    @Environment(\.myNavigationPath) var customValue
    
    var body: some View {
        VStack(alignment: .leading, spacing: 20) {
            NavigationLink("Go Profile", value: AppViewRouter.profile)
            NavigationLink("Go Category List", value: AppViewRouter.category(.list))
            Button("Go Category Detail") {
                customValue.wrappedValue.append(AppViewRouter.category(.detail(999)))
                
            }
            Button("Back") {
                if !customValue.wrappedValue.isEmpty {
                    customValue.wrappedValue.removeLast()
                }
            }
            
            Button("Back to Root") {
                customValue.wrappedValue.removeAll()
            }
        }
        .padding()
        .navigationTitle(title)
    }
    
}

总结

通过上述的例子我们可以看到,使用枚举来定义应用的视图层级是一种非常的好的方式,而关联值也很好的解决了视图的入参问题。将绑定的视图数组注入到环境中,也能让子视图方便的控制整个界面的导航。

整个过程不需要涉及新的内容,都是一些在SwiftUI开发中的常见的部分。但在使用体验上要比之前好得多。

完整Demo地址:Github地址

相关推荐
东坡肘子3 天前
肘子的 Swift 周报 #063|异种肾脏移植取得突破
swiftui·swift·apple
恋猫de小郭4 天前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui
靴子学长5 天前
iOS + watchOS Tourism App(含源码可简单复现)
mysql·ios·swiftui
hxx22111 天前
iOS swift开发系列--如何给swiftui内容视图添加背景图片显示
ios·swiftui·swift
胖虎111 天前
SwiftUI - (十九)组合视图
ios·swiftui·swift·组合视图
davidson147112 天前
Xcode
ios·swiftui·xcode·swift·apple
大熊猫侯佩13 天前
苹果开发者入门:修复 SwiftUI 中“跑偏的”动画(下)
swiftui·动画·animation·transition·转场·显式隐式动画·布局坐标
_rufeng_18 天前
SwiftUI入门篇
ios·swiftui·swift
大熊猫侯佩20 天前
SwiftUI 列表(或 Form)子项中的 Picker 引起导航无法跳转的原因及解决
list·swiftui·form·列表·navigation·导航·picker
袁代码1 个月前
SwiftUI开发教程系列 - 第十二章:本地化与多语言支持
开发语言·前端·ios·swiftui·swift·ios开发