SwiftUI-如何避免将导航硬编码到视图中?


33

我尝试为更大的,可投入生产的SwiftUI App做架构。我一直在遇到一个相同的问题,这表明SwiftUI的一个主要设计缺陷。

仍然没有人能给我完整的工作,生产就绪的答案。

如何在SwiftUI其中包含导航的可重用视图?

由于SwiftUI NavigationLink牢固地绑定在视图上,因此根本不可能在较大的Apps中进行缩放。NavigationLink是的,在这些小样本应用程序中可以工作-是的,但是当您要在一个应用程序中重用许多视图时就不会立即使用。并且可能还会在模块边界上重用。(例如:在iOS,WatchOS等中重用View)

设计问题:NavigationLinks硬编码到视图中。

NavigationLink(destination: MyCustomView(item: item))

但是,如果包含此视图的视图NavigationLink应该可重用,则无法对目的地进行硬编码。必须有一种提供目的地的机制。我在这里问这个问题,得到了很好的答案,但仍然没有完整的答案:

SwiftUI MVVM协调器/路由器/ NavigationLink

想法是将目标链接注入到可重用视图中。通常,此想法可行,但不幸的是,这并不适合实际的Production Apps。一旦有了多个可重复使用的屏幕,我就会遇到一个逻辑问题,即一个可重复使用的视图(ViewA)需要一个预先配置的视图目标(ViewB)。但是,如果ViewB还需要预配置的视图目标该ViewC怎么办?我需要建立ViewB在这样的方式已经ViewC被注入已经ViewB在我注入ViewBViewA。依此类推....但是由于当时必须传递的数据不可用,整个构造失败了。

我的另一个想法是使用Environmentas依赖项注入机制为注入目标NavigationLink。但是我认为,这或多或少应被视为一种骇客行为,而不是针对大型应用程序的可扩展解决方案。我们最终将基本上将环境用于所有内容。但是因为Environment也只能在View内部使用(不能在单独的Coordinators或ViewModels中使用),所以我认为这将再次创建奇怪的构造。

就像业务逻辑(例如,视图模型代码)和视图一样,导航和视图也必须分开(例如,协调器模式)。UIKit这是有可能的,因为我们可以访问视图UIViewControllerUINavigationController在视图后方进行访问。UIKit'sMVC已经存在一个问题,即它混和了许多概念,因此变成了有趣的名称“ Massive-View-Controller”而不是“ Model-View-Controller”。现在,类似的问题仍在继续,SwiftUI但在我看来甚至更糟。导航和视图是紧密耦合的,不能分离。因此,如果包含导航,则无法进行可重用的视图。可以解决这个问题,UIKit但是现在我看不到一个理智的解决方案SwiftUI。不幸的是,Apple没有向我们提供如何解决类似架构问题的解释。我们只有一些小示例应用程序。

我希望证明自己是错误的。请给我展示一个干净的App设计模式,该模式可以解决大量生产就绪的Apps的问题。

提前致谢。


更新:悬赏活动将在几分钟后结束,很遗憾,仍然没有人能够提供有效的示例。但是,如果找不到其他解决方案并将其链接到此处,我将开始提供新的赏金来解决此问题。感谢大家的巨大贡献!


1
同意!几个月前,我在“反馈助手”中为此创建了一个请求,但尚未回复:gist.github.com/Sajjon/b7edb4cc11bcb6462f4e28dc170be245
Sajjon

@Sajjon谢谢!我也打算写Apple,让我们看看是否得到回应。
达科

1
对此写了一封信给苹果。让我们看看是否得到回复。
达科

1
真好!到目前为止,这将是WWDC期间最好的礼物!
Sajjon

Answers:


10

关闭是您所需要的!

struct ItemsView<Destination: View>: View {
    let items: [Item]
    let buildDestination: (Item) -> Destination

    var body: some View {
        NavigationView {
            List(items) { item in
                NavigationLink(destination: self.buildDestination(item)) {
                    Text(item.id.uuidString)
                }
            }
        }
    }
}

我写了一篇关于用闭包替换SwiftUI中的委托模式的文章。 https://swiftwithmajid.com/2019/11/06/the-power-of-closures-in-swiftui/


关闭是个好主意,谢谢!但是在深度视图层次结构中会是什么样子?想象一下,我有一个NavigationView,它可以更深入,更详细,更详细,更详细等10个级别……
Darko

我想邀请您展示一些仅三层的简单示例代码。
达科

7

我的想法几乎是CoordinatorDelegate模式的结合。首先,创建一个Coordinator类:


struct Coordinator {
    let window: UIWindow

      func start() {
        var view = ContentView()
        window.rootViewController = UIHostingController(rootView: view)
        window.makeKeyAndVisible()
    }
}

修改SceneDelegate以使用Coordinator

  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            let coordinator = Coordinator(window: window)
            coordinator.start()
        }
    }

在中ContentView,我们有以下内容:


struct ContentView: View {
    var delegate: ContentViewDelegate?

    var body: some View {
        NavigationView {
            List {
                NavigationLink(destination: delegate!.didSelect(Item())) {
                    Text("Destination1")
                }
            }
        }
    }
}

我们可以这样定义ContenViewDelegate协议:

protocol ContentViewDelegate {
    func didSelect(_ item: Item) -> AnyView
}

哪里Item是可识别的结构,就可以是其他任何结构(例如TableView,UIKit 中某个元素的ID )

下一步是采用此协议,Coordinator并简单地传递您想要呈现的视图:

extension Coordinator: ContentViewDelegate {
    func didSelect(_ item: Item) -> AnyView {
        AnyView(Text("Returned Destination1"))
    }
}

到目前为止,这在我的应用程序中效果很好。希望对您有所帮助。


感谢您提供示例代码。我想邀请您更改Text("Returned Destination1")MyCustomView(item: ItemType, destinationView: View)。因此,这MyCustomView还需要注入一些数据和目标。您将如何解决?
Darko

您遇到我在我的帖子中描述的嵌套问题。如果我错了,请纠正我。基本上,如果你有一个可重复使用的视图和可重复使用的观点并这种方法适用包含与NavigationLink另一种可重复使用的视图。这是一个非常简单的用例,但无法扩展到大型应用程序。(几乎所有视图都可以重用)
Darko

这在很大程度上取决于您如何管理应用程序依存关系及其流程。如果您在一个地方有依赖项,就像IMO(也称为合成根目录)一样,则不应遇到此问题。
Nikola Matijevic

对我有用的是将视图的所有依赖项定义为协议。在组合根目录中为协议添加一致性。将依赖项传递给协调器。从协调员注入它们。从理论上讲,如果正确完成操作,最终应使用三个以上的参数,且不要超过dependenciesdestination
Nikola Matijevic

1
我希望看到一个具体的例子。正如我已经提到的,让我们从开始Text("Returned Destination1")。如果这需要是一个MyCustomView(item: ItemType, destinationView: View)。你打算在那里注入什么?我了解依赖注入,通过协议的松耦合以及与协调器共享的依赖。所有这些都不是问题,而是所需的嵌套。谢谢。
Darko

2

我发生的事情是当你说:

但是,如果ViewB还需要预配置的视图目标ViewC怎么办?我需要先创建ViewB,然后再将ViewB注入到ViewA中,然后再将ViewC注入到ViewB中。依此类推....但是由于当时必须传递的数据不可用,整个构造失败了。

这不是真的。您可以设计可重复使用的组件,而不是提供视图,以便提供可以按需提供视图的闭包。

这样,按需生成ViewB的闭包可以为其提供按需生成ViewC的闭包,但是视图的实际构造可以在您需要的上下文信息可用时发生。


但是,这种“封闭树”的创建与实际观点有何不同?提供项目的问题将得到解决,但不需要嵌套。我创建一个创建视图的闭包-好的。但是在该关闭中,我已经需要提供下一个关闭的创建。最后一个是下一个。等等...但是也许我误会了你。一些代码示例会有所帮助。谢谢。
Darko

2

这是一个有趣的示例,它可以无限地向下钻取并以编程方式更改数据以供下一个详细视图使用

import SwiftUI

struct ContentView: View {
    @EnvironmentObject var navigationManager: NavigationManager

    var body: some View {
        NavigationView {
            DynamicView(viewModel: ViewModel(message: "Get Information", type: .information))
        }
    }
}

struct DynamicView: View {
    @EnvironmentObject var navigationManager: NavigationManager

    let viewModel: ViewModel

    var body: some View {
        VStack {
            if viewModel.type == .information {
                InformationView(viewModel: viewModel)
            }
            if viewModel.type == .person {
                PersonView(viewModel: viewModel)
            }
            if viewModel.type == .productDisplay {
                ProductView(viewModel: viewModel)
            }
            if viewModel.type == .chart {
                ChartView(viewModel: viewModel)
            }
            // If you want the DynamicView to be able to be other views, add to the type enum and then add a new if statement!
            // Your Dynamic view can become "any view" based on the viewModel
            // If you want to be able to navigate to a new chart UI component, make the chart view
        }
    }
}

struct InformationView: View {
    @EnvironmentObject var navigationManager: NavigationManager
    let viewModel: ViewModel

    // Customize your  view based on more properties you add to the viewModel
    var body: some View {
        VStack {
            VStack {
                Text(viewModel.message)
                .foregroundColor(.white)
            }
            .frame(width: 300, height: 300)
            .background(Color.blue)


            NavigationLink(destination: navigationManager.destination(forModel: viewModel)) {
                Text("Navigate")
            }
        }
    }
}

struct PersonView: View {
    @EnvironmentObject var navigationManager: NavigationManager
    let viewModel: ViewModel

    // Customize your  view based on more properties you add to the viewModel
    var body: some View {
        VStack {
            VStack {
                Text(viewModel.message)
                .foregroundColor(.white)
            }
            .frame(width: 300, height: 300)
            .background(Color.red)
            NavigationLink(destination: navigationManager.destination(forModel: viewModel)) {
                Text("Navigate")
            }
        }
    }
}

struct ProductView: View {
    @EnvironmentObject var navigationManager: NavigationManager
    let viewModel: ViewModel

    // Customize your  view based on more properties you add to the viewModel
    var body: some View {
        VStack {
            VStack {
                Text(viewModel.message)
                    .foregroundColor(.white)
            }
            .frame(width: 300, height: 300)
            .background(Color.green)
            NavigationLink(destination: navigationManager.destination(forModel: viewModel)) {
                Text("Navigate")
            }
        }
    }
}

struct ChartView: View {
    @EnvironmentObject var navigationManager: NavigationManager
    let viewModel: ViewModel

    var body: some View {
        VStack {
            VStack {
                Text(viewModel.message)
                    .foregroundColor(.white)
            }
            .frame(width: 300, height: 300)
            .background(Color.green)
            NavigationLink(destination: navigationManager.destination(forModel: viewModel)) {
                Text("Navigate")
            }
        }
    }
}

struct ViewModel {
    let message: String
    let type: DetailScreenType
}

enum DetailScreenType: String {
    case information
    case productDisplay
    case person
    case chart
}

class NavigationManager: ObservableObject {
    func destination(forModel viewModel: ViewModel) -> DynamicView {
        DynamicView(viewModel: generateViewModel(context: viewModel))
    }

    // This is where you generate your next viewModel dynamically.
    // replace the switch statement logic inside with whatever logic you need.
    // DYNAMICALLY MAKE THE VIEWMODEL AND YOU DYNAMICALLY MAKE THE VIEW
    // You could even lead to a view with no navigation link in it, so that would be a dead end, if you wanted it.
    // In my case my "context" is the previous viewMode, by you could make it something else.
    func generateViewModel(context: ViewModel) -> ViewModel {
        switch context.type {
        case .information:
            return ViewModel(message: "Serial Number 123", type: .productDisplay)
        case .productDisplay:
            return ViewModel(message: "Susan", type: .person)
        case .person:
            return ViewModel(message: "Get Information", type: .chart)
        case .chart:
            return ViewModel(message: "Chart goes here. If you don't want the navigation link on this page, you can remove it! Or do whatever you want! It's all dynamic. The point is, the DynamicView can be as dynamic as your model makes it.", type: .information)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
        .environmentObject(NavigationManager())
    }
}

->一些视图会强制您始终只返回一种类型的视图。
Darko

使用EnvironmentObject进行依赖项注入解决了一部分问题。但是:UI框架中至关重要的事物是否应该如此复杂...?
Darko

我的意思是-如果依赖项注入是唯一的解决方案,那么我会无奈地接受它。但这确实会闻起来……
达科

1
我不明白为什么您不能在框架示例中使用它。如果您谈论的是出售未知视图的框架,我想它可能会返回一些View。如果NavigationLink内的AnyView实际上不是那么受欢迎,我也不会感到惊讶,因为父视图与孩子的实际布局完全分开了。我不是专家,但是必须进行测试。而不是要求每个人都无法完全理解您的要求的示例代码,为什么不编写UIKit示例并要求翻译呢?
jasongregori

1
这种设计基本上就是我工作的(UIKit)应用程序的工作方式。生成链接到其他模型的模型。中央系统确定应为该模型加载哪些vc,然后父vc将其推入堆栈。
jasongregori

2

我正在撰写有关在SwiftUI中创建MVP +协调器方法的博客文章系列,这可能会有用:

https://lascorbe.com/posts/2020-04-27-MVPCoordinators-SwiftUI-part1/

完整的项目可在Github上找到:https//github.com/Lascorbe/SwiftUI-MVP-Coordinator

我正在尝试这样做,就好像在可伸缩性方面将是一个大型应用程序一样。我想我已经解决了导航问题,但是我仍然必须看看如何进行深度链接,这是我目前正在研究的内容。希望对您有所帮助。


哇,太好了,谢谢!您在SwiftUI中实现协调器方面做得很好。NavigationView建立根视图的想法很棒。这是迄今为止我所看到的最先进的SwiftUI协调器实现。
Darko

我想授予您赏金,只是因为您的协调器解决方案确实很棒。我唯一的问题-并不能真正解决我描述的问题。它的确解耦,NavigationLink但是通过引入新的耦合依赖关系来实现。在MasterView您的例子并不依赖于NavigationButton。想象一下放置MasterView在Swift包中-由于类型NavigationButton未知,它将不再编译。另外,我不知道如何解决嵌套可重用的问题Views
Darko

我很乐意做错事,如果是的话,请向我解释。即使赏金在几分钟内消失,我希望我能以某种方式奖励您。(从未做过赏金,但我想我可以提出一个新的跟进问题吗?)
Darko

1

这是一个完全不可行的答案,因此可能看起来毫无意义,但我很想使用混合方法。

使用环境来传递单个协调器对象-让我们将其称为NavigationCoordinator。

为您的可重用视图提供某种动态设置的标识符。该标识符提供与客户端应用程序的实际用例和导航层次结构相对应的语义信息。

让可重复使用的视图向NavigationCoordinator查询目标视图,并传递其标识符和要导航到的视图类型的标识符。

这将NavigationCoordinator留为单个注入点,并且它是一个非视图对象,可以在视图层次结构之外进行访问。

在安装过程中,您可以使用运行时传递的标识符进行某种匹配,从而为返回的视图类注册正确的视图类。在某些情况下,与目标标识符匹配之类的简单操作可能会起作用。或与一对主机和目标标识符匹配。

在更复杂的情况下,您可以编写一个自定义控制器,该控制器考虑其他特定于应用程序的信息。

由于是通过环境注入的,因此任何视图都可以在任何时候覆盖默认的NavigationCoordinator,并为其子视图提供不同的视图。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.