SwiftUI - IOS 16 - 如何在 MVVM 架构中使用新的 NavigationStack 和 NavigationPath 进行编程导航?

Áko*_*vai 9 mvvm ios swift swiftui swiftui-navigationstack

描述

对于编程式导航,您之前可以使用NavigationLink(isActive:, destination:, label:) ,当isActive参数为 true时,它​​将触发导航。在 IOS 16 中,这已被弃用,并引入了NavigationStack、NavigationLink(value:, label:)NavigationPath 。

要了解这些的用法,请点击链接:

https://developer.apple.com/documentation/swiftui/migration-to-new-navigation-types https://www.hackingwithswift.com/articles/250/whats-new-in-swiftui-for-ios-16(搜索NavigationStack)

我的问题是,如果我想在不同的视图及其 ViewModel 中使用它,我应该如何使用和维护包含导航堆栈内容的数组(如 NavigationPath 对象)?

正如您在下面的代码中看到的,我创建了一个 NavigationPath 对象来将导航堆栈保存在 BaseView 或 BaseView.ViewModel 中。这样我就可以从该 BaseView 到其他页面(Page1、Page2)进行编程导航,这很棒。

但是,如果我转到 Page1 并尝试以编程方式从那里导航到 Page2,我需要访问原始的 NavigationPath 对象,即我在 BaseView 中使用的对象。

访问这个原始对象的最佳方式是什么?

我可能误解了这个新功能的用法,但如果您有任何可能的从 ViewModel 进行编程导航的解决方案,我会很高兴看到它:)

代码

我尝试做的事情:

struct BaseView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        NavigationStack(path: $viewModel.paths) {
            VStack {
                Button("Page 1", action: viewModel.goToPage1)
                Button("Page 2", action: viewModel.goToPage2)
            }
            .navigationDestination(for: String.self) { stringParam in
                Page1(stringParam: stringParam)
            }
            .navigationDestination(for: Int.self) { intParam in
                Page2(intParam: intParam)
            }
            
        }
    }
}

extension BaseView {
    @MainActor class ViewModel: ObservableObject {
        @Published var paths = NavigationPath()
        
        func goToPage1() {
            let param = "Some random string" // gets the parameter from some calculation or async network call
            
            paths.append(param)
        }
        
        func goToPage2() {
            let param = 19 // gets the parameter from some calculation or async network call
            
            paths.append(param)
        }
    }
}

struct Page1: View {
    @StateObject var viewModel = ViewModel()
    let stringParam: String
    
    var body: some View {
        VStack {
            Button("Page 2", action: viewModel.goToPage2)
        }
    }
}

extension Page1 {
    @MainActor class ViewModel: ObservableObject {
        func goToPage2() {
            // Need to add value to the original paths variable in BaseView.ViewModel
        }
    }
}

struct Page2: View {
    @StateObject var viewModel = ViewModel()
    let intParam: Int
    
    var body: some View {
        Text("\(intParam)")
    }
}

extension Page2 {
    @MainActor class ViewModel: ObservableObject {
    }
}
Run Code Online (Sandbox Code Playgroud)

小智 3

官方迁移指南提供了很多有用的信息。

修饰符navigationDestination(for:destination:)允许对特定数据类型进行自定义处理。

您可以将选定的数据类型“推送”到 上NavigationPath,然后相关navigationDestination块将处理它。

我创建了一些辅助函数来简化新的导航系统。

我将它们存储在一个自定义AppContext类中,您将在下面看到提到的 ( appContext),但当然可以在最适合您自己的代码库的地方放置并引用它们。

    /// The current navigation stack.
    @Published public var navStack = NavigationPath()
    
    /// Type-erased keyed data stored for a given view.
    var navData = Dictionary<String, Any>()
    
    /// Set to `true` the given "show view" bound Bool (i.e. show that view).
    /// Optionally, provide data to pass to that view.
    public func navigateTo(_ showViewFlag: Binding<Bool>,
                      _ navData: Dictionary<String, Any>? = nil) {
        if let navData { self.navData = navData }
        showViewFlag.wrappedValue = true
    }
    
    /// Pop & retrieve navigation data for the given key.
    /// (Generics undo the type-erasure produced by `navigateTo`)
    public func popNavData<T>(_ key: String) -> T {
        navData.removeValue(forKey: key)! as! T
    }
Run Code Online (Sandbox Code Playgroud)

destination修改器是官方修改器的更简洁版本navigationDestination

@ViewBuilder
func destination(`for` show: Binding<Bool>,
                 _ destination: @escaping () -> some View ) -> some View {
    self.navigationDestination(isPresented: show) { DeferView(destination) }
}
Run Code Online (Sandbox Code Playgroud)

DeferView使用的定义为:

import SwiftUI

public struct DeferView<Content: View>: View {
    let content: () -> Content
    public init(@ViewBuilder _ content: @escaping () -> Content) { self.content = content }
    public var body: some View { content() }
}
Run Code Online (Sandbox Code Playgroud)

所以现在你可以这样做:

// Use the standard bound-bool "show view" var format.
@State var showMyView: Bool

// Code to load the view, e.g. in a Button `action`.
navigateTo($showMyView, ["param1": myData1, "param2", myData2])

// Modifier to handle the view load, e.g. on outermost View inside `body`.
.destination(for: $showMyView) {
    MyView(param1: appContext.popNavData("param1"),
           param2: appContext.popNavData("param2"),
           extraParam: $someOtherSource.info)
}
Run Code Online (Sandbox Code Playgroud)