[ios] SwiftUI-뷰에 하드 코딩 된 탐색을 피하는 방법은 무엇입니까?

더 큰 프로덕션 지원 SwiftUI 앱을 위해 아키텍처를 시도합니다. SwiftUI의 주요 디자인 결함을 가리키는 동일한 문제가 발생했습니다.

여전히 아무도 나에게 완전한 작업 준비, 생산 준비 답변을 줄 수 없습니다.

SwiftUI탐색이 포함 된 재사용 가능한 뷰를 수행하는 방법은 무엇입니까?

는 다음과 같이 SwiftUI NavigationLink강력하게 뷰에 바인딩이는 큰 앱도 확장 단순히 같은 방식으로 수 없습니다. NavigationLink작은 샘플 앱에서 작동하지만 예, 하나의 앱에서 많은 뷰를 재사용하려는 즉시 아닙니다. 또한 모듈 경계를 넘어 재사용 할 수도 있습니다. (예 : iOS, WatchOS 등에서 View 재사용 등)

디자인 문제 : NavigationLinks가 뷰에 하드 코딩됩니다.

NavigationLink(destination: MyCustomView(item: item))

그러나 이것을 포함하는보기를 NavigationLink재사용 할 수 있다면 목적지를 하드 코딩 할 수 없습니다 . 대상을 제공하는 메커니즘이 있어야합니다. 나는 이것을 여기에 물었고 꽤 좋은 대답을 얻었지만 여전히 완전한 대답은 아닙니다.

SwiftUI MVVM 코디네이터 / 라우터 / 네비게이션 링크

아이디어는 대상 링크를 재사용 가능한보기에 삽입하는 것이 었습니다. 일반적으로 아이디어가 작동하지만 불행히도 실제 프로덕션 앱으로 확장되지는 않습니다. 재사용 가능한 화면이 여러 개있는 즉시 하나의 재사용 가능한보기 ( ViewA)에 미리 구성된보기 대상 ( ViewB)이 필요 하다는 논리적 문제가 발생합니다 . 그러나 ViewB사전 구성된 뷰 목적지가 필요한 경우 어떻게 해야 ViewC합니까? 내가 만들어야 ViewB하는 방식으로 이미 ViewC이미 주입 ViewB내가 주입하기 전에 ViewBViewA. 그리고 ….. 그 당시에 전달 된 데이터를 사용할 수 없으므로 전체 구문이 실패합니다.

내가 가진 또 다른 아이디어는의 Environment대상을 주입하기 위해 의존성 주입 메커니즘으로 사용하는 것이 었 습니다 NavigationLink. 그러나 이것이 큰 앱을위한 확장 가능한 솔루션이 아니라 해킹으로 간주되어야한다고 생각합니다. 우리는 기본적으로 모든 것을 위해 환경을 사용하게됩니다. 그러나 환경은 View의 내부 에서만 사용할 수 있기 때문에 (별도의 코디네이터 또는 ViewModel이 아닌) 내 의견으로는 이상한 구성을 다시 만듭니다.

비즈니스 로직 (예를 들어보기 모델 코드) 볼처럼 또한 탐색을 분리 할 필요가와에서 (예를 들어, 코디네이터 패턴)을 분리 할 필요가 볼 UIKit우리가에 액세스 할 수 있기 때문에 가능 UIViewController하고 UINavigationController뷰 뒤에. UIKit'sMVC는 이미 “Model-View-Controller”대신 “Massive-View-Controller”라는 재미있는 개념이 될 정도로 많은 개념을 깨뜨리는 문제가있었습니다. 이제 비슷한 문제가 계속 SwiftUI되지만 내 의견으로는 더 나쁩니다. 탐색과보기는 강력하게 연결되어 있으며 분리 할 수 ​​없습니다. 따라서 탐색이 포함 된 경우 재사용 가능한보기를 수행 할 수 없습니다. 이 문제를 해결할 UIKit수 있었지만 이제는 제정신의 해결책을 볼 수 없습니다.SwiftUI. 불행히도 Apple은 그러한 아키텍처 문제를 해결하는 방법에 대한 설명을 제공하지 않았습니다. 작은 샘플 앱이 있습니다.

나는 틀린 것으로 증명되고 싶습니다. 대량 생산 준비가 된 Apps를 위해 이것을 해결하는 깨끗한 App 디자인 패턴을 보여주세요.

미리 감사드립니다.


업데이트 :이 현상금은 몇 분 안에 끝나고 불행히도 아무도 여전히 모범을 보여줄 수 없었습니다. 그러나 다른 솔루션을 찾을 수 없으면 여기에 연결하면이 문제를 해결하기 위해 새로운 현상금을 시작할 것입니다. 그들의 큰 공헌에 감사드립니다!



답변

폐쇄는 당신이 필요한 전부입니다!

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/


답변

내 생각은 거의 패턴 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식별 할 수있는 구조체는 어디에 있습니까 , 다른 것이 될 수 있습니다 (예 : TableViewUIKit에서 와 같은 일부 요소의 id )

다음 단계는이 프로토콜을 채택 Coordinator하고 제시하려는보기를 전달하는 것입니다.

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

이것은 지금까지 내 앱에서 잘 작동했습니다. 도움이 되길 바랍니다.


답변

나에게 일어나는 것은 당신이 말할 때입니다 :

그러나 ViewB에 사전 구성된 View-Destination ViewC가 필요한 경우 어떻게해야합니까? ViewB를 ViewA에 주입하기 전에 ViewB가 ViewB에 이미 주입되어있는 방식으로 ViewB를 작성해야합니다. 그리고 ….. 그 당시에 전달 된 데이터를 사용할 수 없으므로 전체 구문이 실패합니다.

사실이 아닙니다. 뷰를 제공하는 대신 요청시 뷰를 제공하는 클로저를 제공 할 수 있도록 재사용 가능한 구성 요소를 설계 할 수 있습니다.

이렇게하면 주문형 ViewB를 생성하는 클로저가 주문형 ViewC를 생성하는 클로저를 제공 할 수 있지만 필요한 컨텍스트 정보가 제공되는 시점에 실제 뷰 구성이 발생할 수 있습니다.


답변

다음은 무한히 드릴 다운하고 프로그래밍 방식으로 다음 상세 뷰의 데이터를 변경하는 재미있는 예입니다.

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


답변

SwiftUI에서 MVP + 코디네이터 접근 방식을 만드는 방법에 대한 블로그 게시물 시리즈를 작성 중입니다.

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

전체 프로젝트는 Github에서 사용할 수 있습니다 :
https://github.com/Lascorbe/SwiftUI-MVP-Coordinator

확장 성 측면에서 큰 앱 인 것처럼하려고합니다. 내비게이션 문제를 정리했다고 생각하지만 딥 링크를 수행하는 방법을 알아야합니다. 현재 작업 중입니다. 도움이 되길 바랍니다.


답변

이것은 완전히 머리 위의 대답이므로, 말도 안되는 것으로 판명되지만 하이브리드 접근법을 사용하고 싶습니다.

환경을 사용하여 단일 코디네이터 객체를 통과합니다.이를 NavigationCoordinator라고합니다.

재사용 가능한 뷰에 동적으로 설정된 일종의 식별자를 제공하십시오. 이 식별자는 클라이언트 응용 프로그램의 실제 사용 사례 및 탐색 계층에 해당하는 의미 정보를 제공합니다.

재사용 가능한보기가 목적지보기에 대해 NavigationCoordinator를 조회하여 탐색중인보기 유형의 ID 및 ID를 전달하십시오.

이것은 NavigationCoordinator를 단일 주입 지점으로 남겨두고 뷰 계층 외부에서 액세스 할 수있는 비보기 객체입니다.

설정하는 동안 런타임에 전달되는 식별자와 일치하는 종류를 사용하여 반환 할 올바른 뷰 클래스를 등록 할 수 있습니다. 목적지 식별자와 일치하는 것만 큼 간단한 경우가 있습니다. 또는 호스트 및 대상 식별자 쌍과 일치합니다.

더 복잡한 경우에는 다른 앱별 정보를 고려한 사용자 지정 컨트롤러를 작성할 수 있습니다.

환경을 통해 주입되므로 모든보기는 언제라도 기본 Navigation Coordinator를 대체하고 하위보기에 다른보기를 제공 할 수 있습니다.


답변