简体   繁体   English

SwiftUI MVVM 协调器/路由器/导航链接

[英]SwiftUI MVVM Coordinator/Router/NavigationLink

I am having problems to translate UIKit architecture patterns to SwiftUI.我在将 UIKit 架构模式转换为 SwiftUI 时遇到问题。 My current pattern is mostly MVVM with Coordinators/Routers.我目前的模式主要是带有协调器/路由器的 MVVM。 The MVVM part seems quite easy and natural with the addition of @ObservableObject/@Published.添加@ObservableObject/@Published 后,MVVM 部分看起来非常简单自然。 But the coordinating/routing seems unintuitive.但是协调/路由似乎不直观。 The View and the coordination (navigation) functionality are tightly coupled in SwiftUI.视图和协调(导航)功能在 SwiftUI 中紧密耦合。 It seems like it's not really possible to separate them apart from using the helper struct AnyView .似乎真的不可能将它们与使用辅助结构AnyView分开。

Here one example: I want to create a reusable row/cell in SwiftUI.这里有一个例子:我想在 SwiftUI 中创建一个可重复使用的行/单元格。 Let say that this row in Production is quite complex therefore I want to reuse it.假设生产中的这一行非常复杂,因此我想重用它。 I want to place it also in another module so I can reuse it in multiple targets.我也想把它放在另一个模块中,这样我就可以在多个目标中重用它。 (like iOS, macCatalyst, etc...) (如 iOS、macCatalyst 等...)

在此处输入图像描述

Now I want to control what happens when the user taps on that view or buttons in that view.现在我想控制当用户点击该视图或该视图中的按钮时会发生什么。 Depending on the context I need to navigate to different destinations.根据上下文,我需要导航到不同的目的地。 As far I can see the possible NavigationLink targets have to be either hardwired into the view or AnyView has to be passed into the View.据我所知,可能的 NavigationLink 目标必须硬连线到视图中,或者必须将AnyView传递到视图中。

Here some sample code.这里有一些示例代码。 This cell/row contains two buttons.此单元格/行包含两个按钮。 I want to navigate to some other view which is dependent on the context and not to be hardwired into the code:我想导航到依赖于上下文而不是硬连线到代码中的其他视图:

struct ProductFamilyRow: View {
    @State private var selection: Int? = 0
    let item: ProductFamilyItem

    let destinationView1: AnyView
    let destinationView2: AnyView

    var body: some View {
        VStack {
            NavigationLink(
                destination: destinationView1,
                tag: 1,
                selection: self.$selection
            ) {
                EmptyView()
            }

            NavigationLink(
                destination: destinationView2,
                tag: 2,
                selection: self.$selection
            ) {
                EmptyView()
            }

            HStack {
                Text(item.title)
                Button("Destination 1") {
                    self.selection = 1
                }.foregroundColor(Color.blue)

                Button("Destination 2") {
                    self.selection = 2
                }.foregroundColor(Color.blue)
            }

            //Image(item.image)
        }.buttonStyle(PlainButtonStyle())
    }
}

This seems to be a major design flaw in SwiftUI.这似乎是 SwiftUI 中的一个主要设计缺陷。 Reusable components with Navigation Links are basically not possible apart from using the AnyView hack.除了使用AnyView hack 之外,基本上不可能使用带有导航链接的可重用组件。 As far as I know AnyView is just used for specific use cases where I need type-erasure and has quite some performance drawbacks.据我所知, AnyView仅用于需要类型擦除并且具有相当多的性能缺陷的特定用例。 So I do not consider this the idiomatic solution to create reusable, navigatable views with SwiftUI.所以我不认为这是使用 SwiftUI 创建可重用、可导航视图的惯用解决方案。

Is this really the only solution?这真的是唯一的解决方案吗? Maybe I am totally wrong and this is anyway the wrong direction.也许我完全错了,这无论如何都是错误的方向。 I read somewhere (can't find the post anymore..) about using some central state which indicates which view to show but I saw no concrete example how to do this.我在某处读到(再也找不到帖子了..)关于使用一些中央 state 指示要显示哪个视图,但我没有看到如何执行此操作的具体示例。

2nd challenge: Also I do not want the cell to react on any other taps then on the buttons.第二个挑战:我也不希望单元格对任何其他点击做出反应,然后对按钮做出反应。 But it seems not to be possible to control where the cell Navigates to if tapped.但是,如果点击,似乎无法控制单元格导航到的位置。 (so not tapping on one of the buttons but anywhere in the cell) In the current sample code it navigates (for any reason) to "Destination 2". (所以不要点击其中一个按钮,而是点击单元格中的任何位置)在当前示例代码中,它(出于任何原因)导航到“Destination 2”。

Thanks in advance.提前致谢。

It is better to use generics for your row, as below (tested with Xcode 11.4)最好使用 generics 作为您的行,如下所示(使用 Xcode 11.4 测试)

Usage example:使用示例:

ProductFamilyRow(item: ProductFamilyItem(title: "Test"),
    destinationView1: { Text("Details1") },
    destinationView2: { Text("Details2") })

Interface:界面:

Update - added block for row highlight.更新- 为行高亮添加块。 List has auto detection for button or link inside row and highlights if any standard (.key) present, So.列表对行内的按钮或链接具有自动检测功能,如果存在任何标准(.key),则突出显示,所以。 to disable such behaviour it needs to hide everything under custom button style.要禁用这种行为,它需要隐藏自定义按钮样式下的所有内容。

struct ProductFamilyRowStyle: ButtonStyle {

    func makeBody(configuration: Self.Configuration) -> some View {
        configuration.label
            .colorMultiply(configuration.isPressed ? 
                 Color.white.opacity(0.5) : Color.white) // any effect you want
    }
}

struct ProductFamilyRow<D1: View, D2: View>: View {
    let item: ProductFamilyItem
    let destinationView1: () -> D1
    let destinationView2: () -> D2

    init(item: ProductFamilyItem, @ViewBuilder destinationView1: @escaping () -> D1,
        @ViewBuilder destinationView2: @escaping () -> D2)
    {
        self.item = item
        self.destinationView1 = destinationView1
        self.destinationView2 = destinationView2
    }

    @State private var selection: Int? = 0

    var body: some View {
        VStack {
            HStack {
                Text(item.title)
                Button(action: {
                    self.selection = 1
                }) {
                    Text("Destination 1")
                        .background( // hide link inside button !!
                            NavigationLink(destination: destinationView1(),
                                tag: 1, selection: self.$selection) { EmptyView() }
                        )
                }.foregroundColor(Color.blue)

                Button(action: {
                    self.selection = 2
                }) {
                    Text("Destination 2")
                        .background(
                            NavigationLink(destination: destinationView2(),
                                tag: 2, selection: self.$selection) { EmptyView() }
                        )
                }.foregroundColor(Color.blue)
            }

            //Image(item.image)
        }.frame(maxWidth: .infinity) // to have container centered
        .buttonStyle(ProductFamilyRowStyle())
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM