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

问题描述 投票:0回答:2

说明

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

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

https://developer.apple.com/documentation/swiftui/migrating-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 {
    }
}
ios swift swiftui mvvm swiftui-navigationstack
2个回答
0
投票

SwiftUI 中不需要 MVVM,因为

View
结构加属性包装器已经等同于视图模型对象,但速度更快且不易出错。同样在 SwiftUI 中,我们甚至无法访问传统的视图层 - 它采用我们的
View
数据结构,对它们进行差异化以创建/更新/删除
UIView
/
NSView
对象,使用最适合平台的对象/语境。如果您改为使用对象来查看数据,那么您将遇到与 SwiftUI 旨在消除的相同的一致性问题。

可悲的是,网络(和哈佛大学)充斥着 MVVM SwiftUI 文章,这些文章是由那些懒得去正确学习它的人写的。幸运的是,情况正在发生变化:

我错了! MVVM 不是构建 SwiftUI 应用程序的好选择(Azam Sharp)

MVVM 开发者如何在 SwiftUI 中弄错 MVVM:从视图模型到状态(Jim Lai)

停止为 SwiftUI 使用 MVVM(苹果开发者论坛)


0
投票

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

修饰符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
    }

这个

destination
修饰符是官方
navigationDestination
修饰符的更简洁版本:

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

它使用的

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() }
}

现在你可以这样做了:

// 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)
}
© www.soinside.com 2019 - 2024. All rights reserved.