web-dev-qa-db-fra.com

Coordinateur / Routeur / NavigationLink SwiftUI MVVM

J'ai des problèmes pour traduire les modèles d'architecture UIKit vers SwiftUI. Mon modèle actuel est principalement MVVM avec des coordinateurs/routeurs. La partie MVVM semble assez simple et naturelle avec l'ajout de @ ObservableObject/@ Published. Mais la coordination/le routage semble peu intuitif. La vue et la fonctionnalité de coordination (navigation) sont étroitement liées dans SwiftUI. Il semble qu'il n'est pas vraiment possible de les séparer en dehors de l'utilisation de la structure d'assistance AnyView.

Voici un exemple: je veux créer une ligne/cellule réutilisable dans SwiftUI. Disons que cette ligne dans Production est assez complexe, donc je veux la réutiliser. Je veux le placer également dans un autre module afin de pouvoir le réutiliser dans plusieurs cibles. (comme iOS, macCatalyst, etc ...)

enter image description here

Maintenant, je veux contrôler ce qui se passe lorsque l'utilisateur appuie sur cette vue ou sur les boutons de cette vue. Selon le contexte, j'ai besoin de naviguer vers différentes destinations. Pour autant que je puisse voir, les cibles NavigationLink possibles doivent être soit câblées dans la vue ou AnyView doivent être passées dans la vue.

Voici un exemple de code. Cette cellule/ligne contient deux boutons. Je souhaite accéder à une autre vue qui dépend du contexte et ne doit pas être câblée dans le 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())
    }
}

Cela semble être un défaut de conception majeur dans SwiftUI. Les composants réutilisables avec des liens de navigation ne sont fondamentalement pas possibles en dehors de l'utilisation du hack AnyView. Pour autant que je sache, AnyView est juste utilisé pour des cas d'utilisation spécifiques où j'ai besoin d'un effacement de type et présente de nombreux inconvénients en termes de performances. Je ne considère donc pas cela comme la solution idiomatique pour créer des vues réutilisables et navigables avec SwiftUI.

Est-ce vraiment la seule solution? Peut-être que je me trompe totalement et que c'est de toute façon la mauvaise direction. J'ai lu quelque part (je ne trouve plus le message ..) sur l'utilisation d'un état central qui indique la vue à afficher mais je n'ai vu aucun exemple concret comment faire cela.

2ème défi: Je ne veux pas non plus que la cellule réagisse sur d'autres tapotements que sur les boutons. Mais il ne semble pas possible de contrôler la destination de la cellule si elle est activée. (donc ne pas appuyer sur l'un des boutons mais n'importe où dans la cellule) Dans l'exemple de code actuel, il navigue (pour une raison quelconque) vers "Destination 2".

Merci d'avance.

2
Darko

Il est préférable d'utiliser des génériques pour votre ligne, comme ci-dessous (testé avec Xcode 11.4)

Exemple d'utilisation:

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

Interface:

pdate - bloc ajouté pour la mise en évidence de la ligne. La liste a une détection automatique pour le bouton ou le lien à l'intérieur de la ligne et met en évidence si une norme (clé!) Est présente. Donc, pour désactiver ce comportement, il doit tout cacher sous le style de bouton personnalisé.

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