SwiftUI - làm thế nào để tránh điều hướng mã hóa cứng vào chế độ xem?


33

Tôi cố gắng thực hiện kiến ​​trúc cho một ứng dụng SwiftUI lớn hơn, sẵn sàng sản xuất. Tôi luôn luôn gặp phải vấn đề tương tự, điều này chỉ ra một lỗ hổng thiết kế lớn trong SwiftUI.

Vẫn không ai có thể cho tôi một câu trả lời sẵn sàng làm việc, sản xuất.

Làm thế nào để thực hiện Lượt xem có thể sử dụng lại trong SwiftUIđó có điều hướng?

SwiftUI NavigationLinkliên kết chặt chẽ với chế độ xem, điều này chỉ đơn giản là không thể theo cách mà nó cũng mở rộng trong Ứng dụng lớn hơn. NavigationLinktrong các ứng dụng mẫu nhỏ đó hoạt động, có - nhưng không phải ngay khi bạn muốn sử dụng lại nhiều Chế độ xem trong một Ứng dụng. Và cũng có thể tái sử dụng trên các ranh giới mô-đun. (như: sử dụng lại Chế độ xem trong iOS, WatchOS, v.v ...)

Vấn đề thiết kế: NavigationLinks được mã hóa cứng vào View.

NavigationLink(destination: MyCustomView(item: item))

Nhưng nếu chế độ xem chứa mục này NavigationLinkcó thể được sử dụng lại, tôi không thể mã hóa đích đến. Phải có một cơ chế cung cấp đích đến. Tôi đã hỏi điều này ở đây và nhận được một câu trả lời khá tốt, nhưng vẫn không có câu trả lời đầy đủ:

SwiftUI MVVM Điều phối viên / Bộ định tuyến / NavigationLink

Ý tưởng là đưa các Liên kết Đích vào chế độ xem có thể sử dụng lại. Nói chung, ý tưởng hoạt động nhưng thật không may, điều này không mở rộng thành Ứng dụng sản xuất thực sự. Ngay khi tôi có nhiều màn hình có thể sử dụng lại, tôi gặp phải vấn đề logic rằng một chế độ xem có thể sử dụng lại ( ViewA) cần một chế độ xem đích được cấu hình sẵn ( ViewB). Nhưng điều gì sẽ xảy ra nếu ViewBcũng cần một điểm đến được cấu hình sẵn ViewC? Tôi sẽ cần phải tạo ra ViewBmột cách đã ViewCđược tiêm ViewBtrước khi tôi tiêm ViewBvào ViewA. Và như vậy .... nhưng vì dữ liệu tại thời điểm đó phải được thông qua là không có sẵn, toàn bộ cấu trúc thất bại.

Một ý tưởng khác mà tôi có là sử dụng Environmentcơ chế tiêm phụ thuộc để tiêm đích NavigationLink. Nhưng tôi nghĩ rằng điều này nên được coi ít nhiều là một hack và không phải là một giải pháp có thể mở rộng cho các Ứng dụng lớn. Chúng tôi cuối cùng sẽ sử dụng Môi trường cho tất cả mọi thứ. Nhưng vì Môi trường cũng có thể được sử dụng chỉ trong Xem lý lịch thành (không phải trong điều phối viên hoặc ViewModels riêng) này một lần nữa sẽ tạo ra cấu trúc kỳ lạ trong quan điểm của tôi.

Giống như logic nghiệp vụ (ví dụ: mã mô hình chế độ xem) và chế độ xem phải được tách riêng, điều hướng và chế độ xem phải được tách riêng (ví dụ: mẫu Điều phối viên) UIKitCó thể vì chúng tôi truy cập vào UIViewControllerUINavigationControllerphía sau chế độ xem. UIKit'sMVC đã có vấn đề là nó đã trộn lẫn rất nhiều khái niệm để nó trở thành cái tên vui nhộn "Massive-View-Controller" thay vì "Model-View-Controller". Bây giờ một vấn đề tương tự tiếp tục trong SwiftUInhưng theo tôi thậm chí còn tồi tệ hơn. Điều hướng và Chế độ xem được kết hợp chặt chẽ và không thể tách rời. Do đó, không thể thực hiện các chế độ xem có thể sử dụng lại nếu chúng có chứa điều hướng. Có thể giải quyết vấn đề này UIKitnhưng bây giờ tôi không thể thấy một giải pháp lành mạnh trongSwiftUI. Thật không may, Apple đã không cung cấp cho chúng tôi một lời giải thích làm thế nào để giải quyết các vấn đề kiến ​​trúc như thế. Chúng tôi chỉ có một số Ứng dụng mẫu nhỏ.

Tôi rất thích được chứng minh là sai. Vui lòng chỉ cho tôi một mẫu thiết kế Ứng dụng sạch sẽ giải quyết điều này cho các Ứng dụng sẵn sàng sản xuất lớn.

Cảm ơn trước.


Cập nhật: tiền thưởng này sẽ kết thúc sau vài phút và thật không may vẫn không ai có thể cung cấp một ví dụ hoạt động. Nhưng tôi sẽ bắt đầu một tiền thưởng mới để giải quyết vấn đề này nếu tôi không thể tìm thấy bất kỳ giải pháp nào khác và liên kết nó ở đây. Cảm ơn tất cả những đóng góp tuyệt vời của họ!


1
Đã đồng ý! Tôi đã tạo một yêu cầu cho điều này trong Trợ lý phản hồi của hồi giáo cách đây nhiều tháng, chưa có phản hồi nào: gist.github.com/Sajjon/b7edb4cc11bcb6462f4e28dc170be245
Sajjon

@Sajjon Cảm ơn! Tôi cũng có ý định viết Apple, hãy xem tôi có nhận được phản hồi không.
Darko

1
A đã viết một lá thư cho Apple liên quan đến điều này. Chúng ta hãy xem nếu chúng ta có được một repsonse.
Darko

1
Đẹp! Nó sẽ là món quà tốt nhất trong WWDC cho đến nay!
Sajjon

Câu trả lời:


10

Việc đóng cửa là tất cả những gì bạn cần!

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

Tôi đã viết một bài về việc thay thế mẫu đại biểu trong SwiftUI bằng các bao đóng. https://swiftwithmajid.com/2019/11/06/the-power-of-closures-in-swiftui/


Việc đóng cửa là một ý tưởng tốt, cảm ơn! Nhưng làm thế nào mà trông giống như trong một hệ thống phân cấp xem sâu? Hãy tưởng tượng tôi có một NavigationView đi sâu hơn 10 cấp độ, chi tiết, chi tiết, chi tiết, v.v ...
Darko

Tôi muốn mời bạn hiển thị một số mã ví dụ đơn giản chỉ sâu ba cấp độ.
Darko

7

Ý tưởng của tôi sẽ khá nhiều là một sự kết hợp CoordinatorDelegatemô hình. Đầu tiên, tạo một Coordinatorlớp:


struct Coordinator {
    let window: UIWindow

      func start() {
        var view = ContentView()
        window.rootViewController = UIHostingController(rootView: view)
        window.makeKeyAndVisible()
    }
}

Thích ứng với SceneDelegateviệc sử dụng 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()
        }
    }

Bên trong ContentView, chúng ta có cái này:


struct ContentView: View {
    var delegate: ContentViewDelegate?

    var body: some View {
        NavigationView {
            List {
                NavigationLink(destination: delegate!.didSelect(Item())) {
                    Text("Destination1")
                }
            }
        }
    }
}

Chúng ta có thể định nghĩa ContenViewDelegategiao thức như thế này:

protocol ContentViewDelegate {
    func didSelect(_ item: Item) -> AnyView
}

Trường hợp Itemchỉ là một cấu trúc có thể xác định được, có thể là bất cứ thứ gì khác (ví dụ: id của một số phần tử như TableViewtrong UIKit)

Bước tiếp theo là áp dụng giao thức này Coordinatorvà chỉ cần vượt qua chế độ xem bạn muốn trình bày:

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

Điều này cho đến nay đã hoạt động độc đáo trong các ứng dụng của tôi. Tôi hy vọng nó sẽ giúp.


Cảm ơn mã mẫu. Tôi muốn mời bạn thay đổi Text("Returned Destination1")để một cái gì đó như MyCustomView(item: ItemType, destinationView: View). Vì vậy, MyCustomViewcũng cần một số dữ liệu và đích đến được tiêm. Làm thế nào bạn sẽ giải quyết điều đó?
Darko

Bạn gặp vấn đề làm tổ mà tôi mô tả trong bài viết của mình. Xin hãy sửa tôi nếu tôi sai. Về cơ bản phương pháp này hoạt động nếu bạn có một chế độ xem có thể sử dụng lại và chế độ xem có thể sử dụng lại đó không chứa chế độ xem có thể sử dụng lại khác với NavigationLink. Đây là trường hợp sử dụng khá đơn giản nhưng không mở rộng thành Ứng dụng lớn. (nơi hầu hết mọi chế độ xem đều có thể sử dụng lại)
Darko

Điều này phụ thuộc rất nhiều vào cách bạn quản lý các phụ thuộc ứng dụng và luồng của chúng. Nếu bạn đang có sự phụ thuộc ở một nơi duy nhất, vì bạn nên IMO (còn được gọi là gốc thành phần), bạn không nên gặp phải vấn đề này.
Nikola Matijevic

Những gì làm việc cho tôi là xác định tất cả các phụ thuộc của bạn cho một khung nhìn như một giao thức. Thêm sự phù hợp với giao thức trong thư mục gốc thành phần. Truyền phụ thuộc cho điều phối viên. Tiêm chúng từ điều phối viên. Về lý thuyết, bạn nên kết thúc với nhiều hơn ba tham số, nếu được thực hiện đúng cách không bao giờ nhiều hơn dependenciesdestination.
Nikola Matijevic

1
Tôi rất thích nhìn thấy một ví dụ cụ thể. Như tôi đã đề cập, hãy bắt đầu tại Text("Returned Destination1"). Điều gì nếu điều này cần phải là một MyCustomView(item: ItemType, destinationView: View). Bạn sẽ tiêm gì ở đó? Tôi hiểu tiêm phụ thuộc, giao thức thông qua khớp nối lỏng lẻo và chia sẻ phụ thuộc với các điều phối viên. Tất cả điều đó không phải là vấn đề - đó là sự lồng ghép cần thiết. Cảm ơn.
Darko

2

Một cái gì đó xảy ra với tôi là khi bạn nói:

Nhưng điều gì sẽ xảy ra nếu ViewB cũng cần một ViewC đích được định cấu hình sẵn? Tôi sẽ cần phải tạo ViewB theo cách mà ViewC đã được đưa vào ViewB trước khi tôi đưa ViewB vào ViewA. Và như vậy .... nhưng vì dữ liệu tại thời điểm đó phải được thông qua là không có sẵn, toàn bộ cấu trúc thất bại.

nó không hoàn toàn đúng Thay vì cung cấp các khung nhìn, bạn có thể thiết kế các thành phần có thể sử dụng lại để bạn cung cấp các bao đóng cung cấp các khung nhìn theo yêu cầu.

Bằng cách đó, việc đóng cửa tạo ra ViewB theo yêu cầu có thể cung cấp cho nó một bao đóng tạo ra ViewC theo yêu cầu, nhưng việc xây dựng các khung nhìn thực tế có thể xảy ra tại thời điểm có sẵn thông tin theo ngữ cảnh mà bạn cần.


Nhưng làm thế nào để tạo ra „đóng-cây như vậy khác với quan điểm thực tế? Các mục cung cấp vấn đề sẽ được giải quyết, nhưng không phải là lồng cần thiết. Tôi tạo một bao đóng tạo ra một khung nhìn - ok. Nhưng trong lần đóng cửa đó, tôi đã cần phải cung cấp việc tạo ra lần đóng cửa tiếp theo. Và trong cái cuối cùng tiếp theo. V.v ... nhưng có lẽ tôi hiểu nhầm bạn. Một số ví dụ mã sẽ giúp. Cảm ơn.
Darko

2

Dưới đây là một ví dụ thú vị về việc truy sâu vô tận và thay đổi dữ liệu của bạn để xem chi tiết tiếp theo theo chương trình

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

-> một số Chế độ xem buộc bạn phải luôn trả về một loại Chế độ xem.
Darko

Việc tiêm phụ thuộc với môi trường môi trường giải quyết một phần của vấn đề. Nhưng: một cái gì đó quan trọng và quan trọng trong khung UI sẽ quá phức tạp ...?
Darko

Ý tôi là - nếu tiêm phụ thuộc là giải pháp duy nhất cho việc này thì tôi sẽ miễn cưỡng chấp nhận nó. Nhưng điều này thực sự sẽ có mùi ...
Darko

1
Tôi không thấy lý do tại sao bạn không thể sử dụng điều này với ví dụ khung của bạn. Nếu bạn đang nói về một khung công tác có một chế độ xem không xác định, tôi sẽ tưởng tượng nó có thể trả về một số Chế độ xem. Tôi cũng sẽ không ngạc nhiên nếu AnyView bên trong NavigationLink thực sự không phải là một điểm nhấn lớn vì chế độ xem chính hoàn toàn tách biệt với bố cục thực tế của trẻ. Tôi không phải là chuyên gia, nó sẽ phải được kiểm tra. Thay vì hỏi mọi người về mã mẫu nơi họ không thể hiểu đầy đủ các yêu cầu của bạn, tại sao bạn không viết mẫu UIKit và yêu cầu dịch?
jasongregori

1
Thiết kế này về cơ bản là cách ứng dụng (UIKit) tôi hoạt động trên hoạt động. Các mô hình được tạo ra liên kết đến các mô hình khác. Một hệ thống trung tâm xác định những gì vc nên được tải cho mô hình đó và sau đó vc cha đẩy nó lên ngăn xếp.
jasongregori

2

Tôi đang viết một loạt bài đăng trên blog về cách tạo phương pháp Điều phối MVP + trong SwiftUI có thể hữu ích:

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

Dự án đầy đủ có sẵn trên Github: https://github.com/Lascorbe/SwiftUI-MVP-Coordinator

Tôi đang cố gắng làm điều đó như thể nó là một ứng dụng lớn về khả năng mở rộng. Tôi nghĩ rằng tôi đã sắp xếp vấn đề điều hướng, nhưng tôi vẫn phải xem cách thực hiện liên kết sâu, đó là những gì tôi hiện đang làm việc. Tôi hy vọng nó sẽ giúp.


Wow, thật tuyệt, cảm ơn bạn! Bạn đã làm khá tốt khi triển khai các Điều phối viên trong SwiftUI. Ý tưởng để làm cho NavigationViewcác quan điểm gốc là tuyệt vời. Đây là triển khai Điều phối viên SwiftUI tiên tiến nhất mà tôi từng thấy cho đến nay.
Darko

Tôi muốn thưởng cho bạn tiền thưởng chỉ vì giải pháp Điều phối viên của bạn thực sự tuyệt vời. Vấn đề duy nhất tôi có - nó không thực sự giải quyết vấn đề tôi mô tả. Nó tách rời NavigationLinknhưng nó làm như vậy bằng cách giới thiệu một phụ thuộc kết hợp mới. Trong MasterViewví dụ của bạn không phụ thuộc vào NavigationButton. Hãy tưởng tượng việc đặt MasterViewtrong Gói Swift - nó sẽ không biên dịch nữa vì loại NavigationButtonnày không xác định. Ngoài ra tôi không thấy vấn đề tái sử dụng lồng nhau Viewssẽ được giải quyết bằng cách nào?
Darko

Tôi sẽ rất vui khi được sai, và nếu tôi thì hãy giải thích cho tôi. Mặc dù tiền thưởng hết trong vài phút, tôi hy vọng tôi có thể thưởng cho bạn bằng cách nào đó điểm. (chưa bao giờ làm một tiền thưởng trước đây, nhưng tôi nghĩ rằng tôi chỉ có thể tạo một câu hỏi tiếp theo với một câu hỏi mới?)
Darko

1

Đây là một câu trả lời hoàn toàn khó hiểu, vì vậy có lẽ sẽ trở nên vô nghĩa, nhưng tôi muốn sử dụng một phương pháp lai.

Sử dụng môi trường để chuyển qua một đối tượng điều phối viên duy nhất - hãy gọi nó là NavigationCoordinator.

Cung cấp cho các chế độ xem có thể sử dụng lại của bạn một số loại định danh được đặt động. Mã định danh này cung cấp thông tin ngữ nghĩa tương ứng với trường hợp sử dụng thực tế và phân cấp điều hướng của ứng dụng khách.

Có các chế độ xem có thể sử dụng lại truy vấn NavigationCoordinator cho chế độ xem đích, chuyển định danh của chúng và định danh của loại chế độ xem mà chúng đang điều hướng.

Điều này khiến cho NavigationCoordinator là một điểm tiêm duy nhất và nó là một đối tượng không xem có thể được truy cập bên ngoài hệ thống phân cấp chế độ xem.

Trong quá trình thiết lập, bạn có thể đăng ký các lớp xem đúng để nó trả về, sử dụng một số loại khớp với các định danh mà nó được truyền khi chạy. Một cái gì đó đơn giản như khớp với mã định danh đích có thể hoạt động trong một số trường hợp. Hoặc khớp với một cặp định danh máy chủ và đích.

Trong các trường hợp phức tạp hơn, bạn có thể viết một bộ điều khiển tùy chỉnh có tính đến thông tin cụ thể của ứng dụng khác.

Vì nó được tiêm qua môi trường, bất kỳ chế độ xem nào cũng có thể ghi đè Trình điều hướng mặc định tại bất kỳ điểm nào và cung cấp một chế độ khác cho các lần xem xét của nó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.