web-dev-qa-db-ja.com

SwiftUI MVVMコーディネーター/ルーター/ NavigationLink

UIKitアーキテクチャパターンをSwiftUIに変換するのに問題があります。私の現在のパターンは、主にコーディネーター/ルーターを備えたMVVMです。 MVVMの部分は、@ ObservableObject/@ Publishedを追加することで、非常に簡単で自然に見えます。しかし、調整/ルーティングは直感的ではないようです。ビューと調整(ナビゲーション)機能は、SwiftUIで緊密に結合されています。ヘルパー構造体AnyViewを使用する以外に、それらを分離することは実際には不可能のようです。

ここで1つの例:SwiftUIで再利用可能な行/セルを作成します。 Productionのこの行は非常に複雑であるため、再利用したいとします。別のモジュールにも配置して、複数のターゲットで再利用できるようにしたいと考えています。 (iOS、macCatalystなど)

enter image description here

ここで、ユーザーがそのビューまたはそのビューのボタンをタップしたときの動作を制御したいと思います。コンテキストに応じて、別の目的地に移動する必要があります。可能なNavigationLinkターゲットは、ビューにhardwiredするか、ビューにAnyViewを渡す必要があることがわかります。

ここにいくつかのサンプルコードがあります。このセル/行には2つのボタンが含まれています。コンテキストに依存し、コードにハードワイヤーされない他のビューに移動したい:

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

これは、SwiftUIの主要な設計上の欠陥のようです。ナビゲーションリンクを使用した再利用可能なコンポーネントは、AnyViewハックの使用を除いて、基本的には不可能です。私が知る限り、AnyViewは、型消去が必要な特定のユースケースにのみ使用され、かなりのパフォーマンス上の欠点があります。そのため、SwiftUIを使用して再利用可能でナビゲート可能なビューを作成するための慣用的な解決策とは考えていません。

これは本当に唯一の解決策ですか?多分私は完全に間違っており、これはとにかく間違った方向です。どこのビューを表示するかを示す中央の状態の使用についてどこかで(投稿を見つけることができません。)読みましたが、これを行う具体的な例はありませんでした。

2番目の課題:また、セルが他のタップに反応してからボタンに反応することを望みません。ただし、タップすると、セルの移動先を制御できないようです。 (したがって、ボタンの1つではなく、セル内の任意の場所をタップしません)現在のサンプルコードでは、(何らかの理由で)「Destination 2」に移動します。

前もって感謝します。

2
Darko

以下のように、行にジェネリックを使用することをお勧めします(Xcode 11.4でテスト済み)

使用例:

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

インターフェース:

pdate-行の強調表示にブロックを追加しました。リストには、行内のボタンまたはリンクの自動検出があり、標準(!key)が存在する場合は強調表示されます。そのため、このような動作を無効にするには、カスタムボタンスタイルですべてを非表示にする必要があります。

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