SwiftUI中的NavigationStack是iOS 16引进根据数据类型驱动的导航视图。经过将数据类型与视图相关起来,供给了类型安全的运用办法。本文并非介绍NavigationStack根本运用的文章,故这儿就不多做介绍了。

在实际运用中,经过编码的办法保护导航视图栈对错常常见的行为,比方:翻开不同的视图;退出部分视图等。虽然在官方的NavigationStack示例中,也供给了保护导航视图途径的办法,但仅能支撑一些简单的场景。别的,在运用上也存在一些局限性。整体的可操作性与UIKit中的UINavigationController有些差距。

下面先来看看示例文档中途径绑定的比如。

NavigationStack的途径绑定办法

当需求保护多个视图界面时,经过在初始化办法中传入绑定path的数组,然后经过对数组的操作来完结视图的导航,比方:给数组插入元素或许删去元素,即完结界面的翻开和退出。

@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。

@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也支撑简单的数据增加和删去元素的等操作。

/// 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

首先,咱们经过枚举来表明运用中的视图类型,结合嵌套的枚举来表明子级的视图类型。别的,经过相关值来传递子视图或视图的入参。

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中经过不同的枚举来完结对不同的视图创建,经过编译器,也可以协助咱们不会遗失没有处理的视图。

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值,将当时的绑定的枚举数组注入到环境中。


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持续翻开新的视图,以一种类型安全并且带有层级结构的办法。

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地址