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

时间:2020-04-19 12:55:12

标签: ios swift xcode swiftui swiftui-navigationlink

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

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

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

由于SwiftUI NavigationLink与视图紧密地绑定在一起,因此无法在更大的Apps中进行缩放。是的,NavigationLink在这些小示例应用程序中是可以的,但是当您要在一个应用程序中重用许多视图时就不能使用。并且可能还会在模块边界上重用。 (例如:在iOS,WatchOS等中重用View)

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

NavigationLink(destination: MyCustomView(item: item))

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

SwiftUI MVVM Coordinator/Router/NavigationLink

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

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

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

我很想被证明是错误的。请给我展示一个干净的App设计模式,该模式可以解决大规模生产的Apps问题。

谢谢。


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


2020年6月18日更新: 我从苹果公司那里得到了关于这个问题的答案,提出了这样的建议来分离视图和模型:

enum Destination {
  case viewA
  case viewB 
  case viewC
}

struct Thing: Identifiable {
  var title: String
  var destination: Destination
  // … other stuff omitted …
}

struct ContentView {
  var things: [Thing]

  var body: some View {
    List(things) {
      NavigationLink($0.title, destination: destination(for: $0))
    }
  }

  @ViewBuilder
  func destination(for thing: Thing) -> some View {
    switch thing.destination {
      case .viewA:
        return ViewA(thing)
      case .viewB:
        return ViewB(thing)
      case .viewC:
        return ViewC(thing)
    }
  }
}

我的答复是:

感谢您的反馈。但如您所见,您仍然拥有强大的 在视图中耦合。现在,“ ContentView”需要了解所有视图 (ViewA,ViewB,ViewC),它也可以导航。如我所说,这在 小型示例应用程序,但无法扩展到可用于大规模生产的应用程序。

想象一下,我在GitHub的一个项目中创建了一个自定义视图。接着 将此视图导入我的应用程序。此自定义视图什么都不知道 关于它也可以导航的其他视图,因为它们是特定的 到我的应用程序。

我希望我能更好地解释这个问题。

我看到的唯一解决此问题的方法是分离 导航和视图类似于UIKit。 (例如,UINavigationController)

谢谢,达克

因此,仍然没有针对此问题的干净可行的解决方案。期待WWDC2020。


10 个答案:

答案 0 :(得分:14)

关闭就可以了!

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/

答案 1 :(得分:8)

我的想法几乎是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仅是可识别的结构,可以是其他任何内容(例如,UIKit中TableView中某个元素的ID)

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

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

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

答案 2 :(得分:4)

我会尽力回答您的观点。我将举一个小例子,其中我们应该可重用的View是一个简单的View,其中显示了一个Text和一个NavigationLink,它们将用于某些Destination。 如果您想看看我的完整示例,我创建了一个Gist: SwiftUI - Flexible Navigation with Coordinators

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

在您的示例中,该视图已绑定到视图,但是正如已经显示的其他答案一样,您可以将目的地注入到视图类型struct MyView<Destination: View>: View中。您现在可以将符合View的任何Type用作目的地。

但是,如果包含此NavigationLink的视图应该可重用,则无法对目标进行硬编码。必须有一种提供目的地的机制。

通过上述更改,提供了提供类型的机制。一个例子是:

struct BoldTextView: View {
    var text: String

    var body: some View {
        Text(text)
            .bold()
    }
}
struct NotReusableTextView: View {
    var text: String

    var body: some View {
        VStack {
            Text(text)
            NavigationLink("Link", destination: BoldTextView(text: text))
        }
    }
}

将更改为

struct ReusableNavigationLinkTextView<Destination: View>: View {
    var text: String
    var destination: () -> Destination

    var body: some View {
        VStack {
            Text(text)

            NavigationLink("Link", destination: self.destination())
        }
    }
}

您可以像这样传递目的地:

struct BoldNavigationLink: View {
    let text = "Text"
    var body: some View {
        ReusableNavigationLinkTextView(
            text: self.text,
            destination: { BoldTextView(text: self.text) }
        )
    }
}

一旦有了多个可重复使用的屏幕,我就会遇到一个逻辑问题:一个可重复使用的视图(ViewA)需要一个预先配置的视图目标(ViewB)。但是,如果ViewB还需要预配置的视图目标ViewC怎么办?我需要以将ViewC注入ViewB之前已经将ViewC注入的方式创建ViewB。依此类推....

好吧,显然,您需要某种逻辑来确定您的Destination。在某些时候,您需要告诉视图下一步是什么视图。我想您想避免的是这样:

struct NestedMainView: View {
    @State var text: String

    var body: some View {
        ReusableNavigationLinkTextView(
            text: self.text,
            destination: {
                ReusableNavigationLinkTextView(
                    text: self.text,
                    destination: {
                        BoldTextView(text: self.text)
                    }
                )
            }
        )
    }
}

我整理了一个简单的示例,该示例使用Coordinator来传递依赖关系并创建视图。有一个针对协调器的协议,您可以基于此协议实现特定的用例。

protocol ReusableNavigationLinkTextViewCoordinator {
    associatedtype Destination: View
    var destination: () -> Destination { get }

    func createView() -> ReusableNavigationLinkTextView<Destination>
}

现在,我们可以创建一个特定的协调器,当单击BoldTextView时将显示NavigationLink

struct ReusableNavigationLinkShowBoldViewCoordinator: ReusableNavigationLinkTextViewCoordinator {
    @Binding var text: String

    var destination: () -> BoldTextView {
        { return BoldTextView(text: self.text) }
    }

    func createView() -> ReusableNavigationLinkTextView<Destination> {
        return ReusableNavigationLinkTextView(text: self.text, destination: self.destination)
    }
}

如果需要,还可以使用Coordinator来实现用于确定视图目的地的自定义逻辑。以下协调员在单击链接四次后显示ItalicTextView

struct ItalicTextView: View {
    var text: String

    var body: some View {
        Text(text)
            .italic()
    }
}
struct ShowNavigationLinkUntilNumberGreaterFourThenItalicViewCoordinator: ReusableNavigationLinkTextViewCoordinator {
    @Binding var text: String
    let number: Int
    private var isNumberGreaterThan4: Bool {
        return number > 4
    }

    var destination: () -> AnyView {
        {
            if self.isNumberGreaterThan4 {
                let coordinator = ItalicTextViewCoordinator(text: self.text)
                return AnyView(
                    coordinator.createView()
                )
            } else {
                let coordinator = ShowNavigationLinkUntilNumberGreaterFourThenItalicViewCoordinator(
                    text: self.$text,
                    number: self.number + 1
                )
                return AnyView(coordinator.createView())
            }
        }
    }

    func createView() -> ReusableNavigationLinkTextView<AnyView> {
        return ReusableNavigationLinkTextView(text: self.text, destination: self.destination)
    }
}

如果您有需要传递的数据,请在另一个协调器周围创建另一个协调器以保存值。在此示例中,我有一个TextField-> EmptyView-> Text,其中TextField中的值应传递给Text. EmptyView不得包含此信息。

struct TextFieldView<Destination: View>: View {
    @Binding var text: String
    var destination: () -> Destination

    var body: some View {
        VStack {
            TextField("Text", text: self.$text)

            NavigationLink("Next", destination: self.destination())
        }
    }
}

struct EmptyNavigationLinkView<Destination: View>: View {
    var destination: () -> Destination

    var body: some View {
        NavigationLink("Next", destination: self.destination())
    }
}

这是通过调用其他协调器(或自行创建视图)来创建视图的协调器。它将值从TextField传递到Text,而EmptyView对此一无所知。

struct TextFieldEmptyReusableViewCoordinator {
    @Binding var text: String

    func createView() -> some View {
        let reusableViewBoldCoordinator = ReusableNavigationLinkShowBoldViewCoordinator(text: self.$text)
        let reusableView = reusableViewBoldCoordinator.createView()

        let emptyView = EmptyNavigationLinkView(destination: { reusableView })

        let textField = TextFieldView(text: self.$text, destination: { emptyView })

        return textField
    }
}

总结起来,您还可以创建一个MainView,该逻辑具有决定应使用哪种View / Coordinator的逻辑。

struct MainView: View {
    @State var text = "Main"

    var body: some View {
        NavigationView {
            VStack(spacing: 32) {
                NavigationLink("Bold", destination: self.reuseThenBoldChild())
                NavigationLink("Reuse then Italic", destination: self.reuseThenItalicChild())
                NavigationLink("Greater Four", destination: self.numberGreaterFourChild())
                NavigationLink("Text Field", destination: self.textField())
            }
        }
    }

    func reuseThenBoldChild() -> some View {
        let coordinator = ReusableNavigationLinkShowBoldViewCoordinator(text: self.$text)
        return coordinator.createView()
    }

    func reuseThenItalicChild() -> some View {
        let coordinator = ReusableNavigationLinkShowItalicViewCoordinator(text: self.$text)
        return coordinator.createView()
    }

    func numberGreaterFourChild() -> some View {
        let coordinator = ShowNavigationLinkUntilNumberGreaterFourThenItalicViewCoordinator(text: self.$text, number: 1)
        return coordinator.createView()
    }

    func textField() -> some View {
        let coordinator = TextFieldEmptyReusableViewCoordinator(text: self.$text)
        return coordinator.createView()
    }
}

我知道我也可以创建一个Coordinator协议和一些基本方法,但是我想展示一个有关如何使用它们的简单示例。

顺便说一句,这与我在Swift Coordinator应用中使用UIKit的方式非常相似。

如果您有任何疑问,反馈或需要改进的地方,请告诉我。

答案 3 :(得分:3)

我发生的事情是当你说:

但是,如果ViewB还需要预配置的视图目标ViewC怎么办?我需要以将ViewC注入ViewB之前已经将ViewC注入的方式创建ViewB。依此类推....但是由于当时必须传递的数据不可用,整个构造失败了。

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

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

答案 4 :(得分:3)

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

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

答案 5 :(得分:2)

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

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

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

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

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

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

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

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

答案 6 :(得分:1)

问题在于静态类型检查,即。要构建NavigationLink,我们需要为其提供一些特定的视图。因此,如果我们需要打破这种依赖关系,我们需要类型擦除,即。 AnyView

这是一个基于想法的可行演示,它基于Router / ViewModel概念,使用了类型擦除视图来避免紧密的依赖关系。在Xcode 11.4 / iOS 13.4上进行了测试。

让我们从所获得的结果开始并进行分析(在评论中):

struct DemoContainerView: View {
    var router: Router       // some router
    var vm: [RouteModel]     // some view model having/being route model

    var body: some View {
        RouteContainer(router: router) {    // route container with UI layout
          List {
            ForEach(self.vm.indices, id: \.self) {
              Text("Label \($0)")
                .routing(with: self.vm[$0])    // modifier giving UI element
                                               // possibility to route somewhere
                                               // depending on model
            }
          }
        }
    }
}

struct TestRouter_Previews: PreviewProvider {
    static var previews: some View {
        DemoContainerView(router: SimpleRouter(), 
            vm: (1...10).map { SimpleViewModel(text: "Item \($0)") })
    }
}

因此,我们拥有纯净的UI,没有任何导航细节,并且对该UI可以路由到的位置有了单独的了解。这是它的工作方式:

demo

构建基块:

// Base protocol for route model
protocol RouteModel {}  

// Base protocol for router
protocol Router {
    func destination(for model: RouteModel) -> AnyView
}

// Route container wrapping NavigationView and injecting router
// into view hierarchy
struct RouteContainer<Content: View>: View {
    let router: Router?

    private let content: () -> Content
    init(router: Router? = nil, @ViewBuilder _ content: @escaping () -> Content) {
        self.content = content
        self.router = router
    }

    var body: some View {
        NavigationView {
            content()
        }.environment(\.router, router)
    }
}

// Modifier making some view as routing element by injecting
// NavigationLink with destination received from router based
// on some model
struct RouteModifier: ViewModifier {
    @Environment(\.router) var router
    var rm: RouteModel

    func body(content: Content) -> some View {
        Group {
            if router == nil {
                content
            } else {
                NavigationLink(destination: router!.destination(for: rm)) { content }
            }
        }
    }
}

// standard view extension to use RouteModifier
extension View {
    func routing(with model: RouteModel) -> some View {
        self.modifier(RouteModifier(rm: model))
    }
}

// Helper environment key to inject Router into view hierarchy
struct RouterKey: EnvironmentKey {
    static let defaultValue: Router? = nil
}

extension EnvironmentValues {
    var router: Router? {
        get { self[RouterKey.self] }
        set { self[RouterKey.self] = newValue }
    }
}

演示中显示的测试代码:

protocol SimpleRouteModel: RouteModel {
    var next: AnyView { get }
}

class SimpleViewModel: ObservableObject {
    @Published var text: String
    init(text: String) {
        self.text = text
    }
}

extension SimpleViewModel: SimpleRouteModel {
    var next: AnyView {
        AnyView(DemoLevel1(rm: self))
    }
}

class SimpleEditModel: ObservableObject {
    @Published var vm: SimpleViewModel
    init(vm: SimpleViewModel) {
        self.vm = vm
    }
}

extension SimpleEditModel: SimpleRouteModel {
    var next: AnyView {
        AnyView(DemoLevel2(em: self))
    }
}

class SimpleRouter: Router {
    func destination(for model: RouteModel) -> AnyView {
        guard let simpleModel = model as? SimpleRouteModel else {
            return AnyView(EmptyView())
        }
        return simpleModel.next
    }
}

struct DemoLevel1: View {
    @ObservedObject var rm: SimpleViewModel

    var body: some View {
        VStack {
            Text("Details: \(rm.text)")
            Text("Edit")
                .routing(with: SimpleEditModel(vm: rm))
        }
    }
}

struct DemoLevel2: View {
    @ObservedObject var em: SimpleEditModel

    var body: some View {
        HStack {
            Text("Edit:")
            TextField("New value", text: $em.vm.text)
        }
    }
}

struct DemoContainerView: View {
    var router: Router
    var vm: [RouteModel]

    var body: some View {
        RouteContainer(router: router) {
            List {
                ForEach(self.vm.indices, id: \.self) {
                    Text("Label \($0)")
                        .routing(with: self.vm[$0])
                }
            }
        }
    }
}

// MARK: - Preview
struct TestRouter_Previews: PreviewProvider {
    static var previews: some View {
        DemoContainerView(router: SimpleRouter(), vm: (1...10).map { SimpleViewModel(text: "Item \($0)") })
    }
}

答案 7 :(得分:0)

一个非常有趣的话题,你们正在这里讨论。在这里,我将分享我的想法。我确实尝试主要关注该问题,而不是对此过多考虑。

比方说,您正在构建一个UI组件框架,您需要在全球的公司内发布该框架。然后,您需要构建“虚拟”组件,这些组件现在将如何展示自己和一些最少的知识,例如它们是否可能具有导航功能。

假设:

  • ViewA组件将存在于UI隔离的框架中。
  • ViewA组件很可能知道从那里可以导航。但是ViewA并不关心其中所包含的内容的类型。它只会提供它自己的“潜在”可导航视图,仅此而已。因此,将要建立的“合同”是。高阶分量 删除的类型化生成器(受React启发,他会在iOS:D多年后告诉我),它将从组件中接收视图。并且此构建器将提供一个View。而已。 ViewA不需要了解其他任何信息。

ViewA

/// UI Library Components framework.

struct ViewAPresentable: Identifiable {
    let id = UUID()
    let text1: String
    let text2: String
    let productLinkTitle: String
}

struct ViewA: View {
    let presentable: ViewAPresentable
    let withNavigationBuilder: (_ innerView: AnyView) -> AnyView

    var body: some View {
        VStack(alignment: .leading,
               spacing: 10) {
            HStack(alignment: .firstTextBaseline,
                   spacing: 8) {
                    Text(presentable.text1)
                    Text(presentable.text2)
                }

                withNavigationBuilder(AnyView(Text(presentable.productLinkTitle)))
        }
    }
}

然后;

  • 我们有一个HostA,它将使用该组件,并且实际上想在该HOC上提供可导航的链接。
/// HOST A: Consumer of that component.

struct ConsumerView: View {
    let presentables: [ViewAPresentable] = (0...10).map {
        ViewAPresentable(text1: "Hello",
                         text2: "I'm \($0)",
            productLinkTitle: "Go to product")
    }

    var body: some View {
        NavigationView {
            List(presentables) {
                ViewA(presentable: $0) { innerView in
                    AnyView(NavigationLink(destination: ConsumerView()) {
                        innerView
                    })
                }
            }
        }
    }
}

但实际上,另一个消费者B。不想提供可导航的链接,它仅提供内部组件,因为在消费者B中的要求是不可导航。

/// HOST B: Consumer of that component. (But here it's not navigatable)

struct ConsumerBView: View {
    let presentables: [ViewAPresentable] = (0...10).map {
        ViewAPresentable(text1: "Hello",
                         text2: "I'm \($0)",
            productLinkTitle: "Product description not available")
    }

    var body: some View {
        NavigationView {
            List(presentables) {
                ViewA(presentable: $0) { innerView in
                    AnyView(innerView)
                }
            }
        }
    }
}

通过检查上面的代码,我们可以建立具有最低限度的最低合同的隔离组件。我去了类型擦除,因为实际上在这里,上下文隐式地需要类型擦除。 ViewA实际上并不关心在其中放置什么。将由消费者负责。

然后基于此,您可以使用FactoryBuilders,Coordinators等进一步抽象解决方案。但实际上,它解决了问题的根源。

答案 8 :(得分:0)

我决定也要解决这个问题。

可以轻松地说,通过环境进行依赖注入将是一种更清洁的方法,实际上在很多方面都可以,但是我决定反对,因为它不允许在目的地站点将通用数据类型用作上下文信息。决心。换句话说,您必须事先对泛型进行专门化,才能将其注入环境。

这是我决定改用的模式...

在框架方面

Segue协调协议

该解决方案的核心是一种协议Segueing

protocol Segueing {
    associatedtype Destination: View
    associatedtype Segue
    
    func destination(for segue: Segue) -> Destination
}

它的作用是定义一种合同,任何附加到视图的Segue协调者必须能够响应于具体的Segue而提供另一个视图作为目的地。

请注意,segue不必是枚举,但是使用由关联类型扩展的有限枚举来实现此目的必要的上下文是可行的。

Segue枚举

enum Destinations<Value> {
    case details(_ context: Value)
}

在此示例中,定义了一个单独的“详细信息”,并采用类型安全的方式采用任意类型的Value来承载用户选择的上下文。 对于单个紧密结合在一起的视图使用单个segue枚举,还是让每个视图定义自己的视图,这是一种设计选择。如果每个视图都带有自己的通用类型,则后者是更可取的选择。

查看

struct ListView<N: Segueing, Value>: View where N.Segue == Destinations<Value>, Value: CustomStringConvertible & Hashable {
    var segues: N
    var items: [Value]
    
    var body: some View {
        NavigationView {
            List(items, id: \.self) { item in
                NavigationLink(destination: self.segues.destination(for: .details(item))) {
                    Text("\(item.description)")
                }
            }
        }
    }
}

以下是通用Value类型的列表视图的示例。我们还建立了Segue协调器N: Segueing和Segue枚举Destinations之间的关系。因此,该视图接受一个Segue协调器,该协调器根据Destinations中可用的Segue响应目的地查询,并将用户选择的值传递给Coordinator进行决策。

可以通过有条件地扩展视图并引入一个新的便捷初始化程序来定义默认的segue协调器,如下所示。

extension ListView where N == ListViewSegues<Value> {
    init(items: [Value]) {
        self = ListView(segues: ListViewSegues(), items: items)
    }
}

这都是在框架或快速包中定义的。

在客户端

Segue协调员

struct ListViewSegues<Value>: Segueing where Value: CustomStringConvertible {
    func destination(for segue: Destinations<Value>) -> some View {
        switch segue {
            case .details(let value):
            return DetailView(segues: DetailViewSegues(), value: value)
        }
    }
}

struct DetailViewSegues<Value>: Segueing where Value: CustomStringConvertible {
    func destination(for segue: Destinations<Value>) -> some View {
        guard case let .details(value) = segue else { return AnyView(EmptyView()) }
        return AnyView(Text("Final destination: \(value.description)")
                .foregroundColor(.white)
                .padding()
                .background(Capsule()
                .foregroundColor(.gray))
        )
    }
}

在客户端,我们需要创建一个Segue协调器。上面我们可以看到通过实例化框架DetailView的另一个视图来响应单个选择的示例。我们提供了另一个Segue协调器,并将(用户选择的)值传递给详细视图。

在呼叫站点

var v1 = ListView(segues: ListViewSegues(), items: [7, 5, 12])
var v2 = ListView(segues: ListViewSegues(), items: ["New York", "Tokyo", "Paris"])
var v3 = ListView(items: ["New York", "Tokyo", "Paris"])

好处

  1. 可以使视图可重用并将其分解为单独的模块 例如框架或快速包。
  2. 可以在客户端上自定义导航目标,而无需预先配置。
  3. 强大的(上下文)类型信息在视图构建站点上可用。
  4. 深层视图层次结构不会导致嵌套的闭包。

答案 9 :(得分:0)

我已将解决方案发布在文章Routing in SwiftUI. Two solutions for routing in SwiftUI中。

以下是概述:

1。带有触发视图的路由器。路由器将为所有可能的导航路线返回触发子视图,以将其插入到呈现视图中。这样的子视图代码段中将包含 NavigationLink .sheet 修饰符,以及指定的目标视图,并将通过绑定使用存储在路由器中的状态属性。这样,呈现视图将不取决于导航代码和目的地,而仅取决于路由器协议。

一个演示视图示例:

protocol PresentingRouterProtocol: NavigatingRouter {
    func presentDetails<TV: View>(text: String, triggerView: @escaping () -> TV) -> AnyView
}

struct PresentingView<R: PresentingRouterProtocol>: View {

    @StateObject private var router: R

    init(router: R) {
        _router = StateObject(wrappedValue: router)
    }

    var body: some View {
        NavigationView {
            router.presentDetails(text: "Details") {
                Text("Present Details")
                    .padding()
            }
        }
    }
}

路由器示例:

class PresentingRouter: PresentingRouterProtocol {

    struct NavigationState {
        var presentingDetails = false
    }

    @Published var navigationState = NavigationState()

    func presentDetails<TV: View>(text: String, triggerView: @escaping () -> TV) -> AnyView {
        let destinationView = PresentedView(text: text, router: BasePresentedRouter(isPresented: binding(keyPath: \.presentingDetails)))
        return AnyView(SheetButton(isPresenting: binding(keyPath: \.presentingDetails), contentView: triggerView, destinationView: destinationView))
    }
}

SheetButton 触发器视图:

struct SheetButton<CV: View, DV: View>: View {

    @Binding var isPresenting: Bool

    var contentView: () -> CV
    var destinationView: DV

    var body: some View {
        Button(action: {
            self.isPresenting = true
        }) {
            contentView()
                .sheet(isPresented: $isPresenting) {
                    self.destinationView
                }
        }
    }
}

源代码:https://github.com/ihorvovk/Routing-in-SwiftUI-with-trigger-views

2。具有类型已删除修饰符的Router。演示视图将配置有用于显示任何其他视图的常规修饰符: .navigation(router) .sheet(路由器) 。用路由器初始化时,这些修饰符将通过绑定跟踪存储在路由器中的导航状态,并在路由器更改状态时执行导航。路由器还将具有用于所有可能导航的功能。这些功能将改变状态并触发导航。

一个演示视图示例:

protocol PresentingRouterProtocol: Router {
    func presentDetails(text: String)
}

struct PresentingView<R: PresentingRouterProtocol>: View {

    @StateObject private var router: R

    init(router: R) {
        _router = StateObject(wrappedValue: router)
    }

    var body: some View {
        NavigationView {
            Button(action: {
                router.presentDetails(text: "Details")
            }) {
                Text("Present Details")
                    .padding()
            }.navigation(router)
        }.sheet(router)
    }
}

自定义 .sheet 修饰符将路由器作为参数:

struct SheetModifier: ViewModifier {

    @Binding var presentingView: AnyView?

    func body(content: Content) -> some View {
        content
            .sheet(isPresented: Binding(
                get: { self.presentingView != nil },
                set: { if !$0 {
                    self.presentingView = nil
                }})
            ) {
                self.presentingView
            }
    }
}

路由器 基类:

class Router: ObservableObject {

    struct State {
        var navigating: AnyView? = nil
        var presentingSheet: AnyView? = nil
        var isPresented: Binding<Bool>
    }

    @Published private(set) var state: State

    init(isPresented: Binding<Bool>) {
        state = State(isPresented: isPresented)
    }
}

子类仅需要实现可用路由的功能:

class PresentingRouter: Router, PresentingRouterProtocol {

    func presentDetails(text: String) {
        let router = Router(isPresented: isNavigating)
        navigateTo (
            PresentedView(text: text, router: router)
        )
    }
}

源代码:https://github.com/ihorvovk/Routing-in-SwiftUI-with-type-erased-modifiers

两种解决方案都将导航逻辑与视图层分开。两者都将导航状态存储在路由器中。它使我们能够简单地通过更改路由器的状态来执行导航并实现深层链接。