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


33

더 큰 프로덕션 지원 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 디자인 패턴을 보여주세요.

미리 감사드립니다.


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


1
동의했다! 몇 달 전에“피드백 지원”에서이 요청을 만들었지
Sajjon

@Sajjon 감사합니다! Apple도 작성하려고하는데 응답이 있는지 봅시다.
Darko

1
이에 관해 애플에 편지를 썼다. 우리가 응답을 얻는 지 봅시다.
Darko

1
좋은! WWDC에서 지금까지 최고의 선물이 될 것입니다!
Sajjon

답변:


10

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

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/


폐쇄는 좋은 생각입니다, 감사합니다! 그러나 딥 뷰 계층 구조에서는 어떻게 보일까요? 내가 10 단계 더 깊고, 상세하게, 상세하게, 상세하게, 등을 탐색하는 NavigationView가 있다고 상상해보십시오.
Darko

3 단계 깊이의 간단한 예제 코드를 보여 드리도록 초대합니다.
Darko

7

내 생각은 거의 패턴 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"))
    }
}

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


샘플 코드에 감사드립니다. 님 Text("Returned Destination1")이 다음과 같이 변경하도록 초대 하고 싶습니다 MyCustomView(item: ItemType, destinationView: View). 따라서 MyCustomView일부 데이터와 대상을 주입해야합니다. 어떻게 해결하겠습니까?
Darko

내 게시물에서 설명하는 중첩 문제가 발생합니다. 내가 틀렸다면 정정 해주세요. 기본적으로이 방법은 재사용 가능한 뷰가 하나 있고 재사용 가능한 뷰에 NavigationLink를 사용하는 다른 재사용 가능한 뷰가 포함되어 있지 않은 경우 작동합니다 . 이것은 매우 간단한 사용 사례이지만 큰 앱으로 확장되지는 않습니다. (거의 모든 뷰를 재사용 할 수있는 곳)
Darko

이는 앱 종속성 및 흐름을 관리하는 방법에 따라 크게 다릅니다. IMO (컴포지션 루트라고도 함)와 같이 단일 위치에 종속성이있는 경우이 문제가 발생하지 않아야합니다.
Nikola Matijevic

나를 위해 작동하는 것은 뷰에 대한 모든 종속성을 프로토콜로 정의하는 것입니다. 컴포지션 루트의 프로토콜에 적합성을 추가하십시오. 코디네이터에게 종속성을 전달하십시오. 코디네이터에서 주입하십시오. 결코 이상 제대로하지 않는 경우 이론적으로, 당신은 이상의 세 가지 매개 변수와 끝까지해야 dependencies하고 destination.
Nikola Matijevic

1
구체적인 예를보고 싶습니다. 이미 언급했듯이에서 시작하겠습니다 Text("Returned Destination1"). 이 요구는 무엇을 할 수 있다면 MyCustomView(item: ItemType, destinationView: View). 거기에 무엇을 주사 할 예정입니까? 나는 의존성 주입, 프로토콜을 통한 느슨한 결합, 코디네이터와의 의존성을 이해합니다. 이 모든 것이 문제가 아닙니다. 필요한 중첩입니다. 감사.
Darko

2

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

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

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

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


그러나 그러한“폐쇄 나무”의 생성은 실제 관점과 어떻게 다릅니 까? 항목 제공 문제는 해결되었지만 필요한 중첩은 해결되지 않았습니다. 뷰를 만드는 클로저를 만듭니다. 그러나 그 마감에서 나는 이미 다음 마감의 창조를 제공해야 할 것입니다. 그리고 마지막 것에서 다음 것. 기타 ...하지만 난 당신을 오해 할 수 있습니다. 일부 코드 예제가 도움이 될 것입니다. 감사.
Darko

2

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

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

-> 일부보기는 항상 한 가지 유형의보기 만 반환하도록합니다.
Darko

EnvironmentObject를 사용한 의존성 주입은 문제의 한 부분을 해결합니다. 그러나 : UI 프레임 워크에서 중요하고 중요한 것이 너무 복잡해야합니까?
Darko

내 말은-의존성 주입이 이것에 대한 유일한 해결책이라면 마지 못해 받아 들일 것입니다. 그러나 이것은 정말로 냄새가납니다.
Darko

1
왜 프레임 워크 예제에서 이것을 사용할 수 없는지 모르겠습니다. 알 수없는보기를 제공하는 프레임 워크에 대해 이야기하고 있다면보기를 반환 할 수 있다고 생각합니다. 부모보기가 자식의 실제 레이아웃과 완전히 분리되어 NavigationLink 내부의 AnyView가 실제로 큰 타격을받지 않으면 놀라지 않을 것입니다. 나는 전문가가 아니지만 테스트를 받아야합니다. 모든 사용자가 요구 사항을 완전히 이해할 수없는 샘플 코드를 요구하는 대신 왜 UIKit 샘플을 작성하고 번역을 요청하지 않습니까?
jasongregori

1
이 디자인은 기본적으로 내가 작업하는 (UIKit) 앱이 작동하는 방식입니다. 다른 모델과 연결되는 모델이 생성됩니다. 중앙 시스템은 해당 모델에 대해 어떤 vc를로드해야하는지 결정한 다음 상위 vc가이를 스택에 푸시합니다.
jasongregori

2

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

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

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

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


와우, 감사합니다! SwiftUI에서 코디네이터를 구현하는 데 상당한 일을했습니다. NavigationView루트 뷰 를 만드는 아이디어 는 환상적입니다. 이것은 지금까지 내가 본 가장 고급 SwiftUI Coordinators 구현입니다.
Darko

코디네이터 솔루션이 정말 훌륭하기 때문에 현상금을 수여하고 싶습니다. 내가 가진 유일한 문제-그것은 내가 설명하는 문제를 실제로 다루지는 않습니다. 분리 NavigationLink되지만 새로운 결합 종속성을 도입하여 그렇게합니다. MasterView당신의 예제가에 의존하지 않는다 NavigationButton. MasterView스위프트 패키지에 배치한다고 상상해보십시오 NavigationButton. 유형 을 알 수 없기 때문에 더 이상 컴파일되지 않습니다 . 또한 중첩 재사용 문제가 어떻게 해결되는지 알지 못 Views합니까?
Darko

나는 틀렸다는 것이 행복 할 것이며, 그렇다면 내가 설명해주십시오. 현상금이 몇 분 안에 없어지더라도 나는 당신에게 어떻게 든 포인트를 수여하기를 바랍니다. (전에는 현상금을 한 적이 없지만 새로운 질문으로 후속 질문을 만들 수 있다고 생각합니까?)
Darko

1

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

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

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

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

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

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

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

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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.