SwiftUI - bagaimana cara menghindari navigasi yang disandikan dalam tampilan?

33

Saya mencoba melakukan arsitektur untuk aplikasi SwiftUI yang lebih besar dan siap produksi. Saya menjalankan semua waktu ke masalah yang sama yang menunjuk ke cacat desain utama di SwiftUI.

Masih tidak ada yang bisa memberi saya jawaban yang berfungsi penuh, siap produksi.

Bagaimana cara melakukan Tampilan yang dapat digunakan kembali SwiftUIyang mengandung navigasi?

Karena SwiftUI NavigationLinksangat terikat pada tampilan ini tidak mungkin sedemikian rupa sehingga skala juga dalam Aplikasi yang lebih besar. NavigationLinkdalam contoh kecil Aplikasi itu berfungsi, ya - tetapi tidak segera setelah Anda ingin menggunakan kembali banyak Tampilan dalam satu Aplikasi. Dan mungkin juga menggunakan kembali batas-batas modul. (seperti: menggunakan kembali Tampilan di iOS, WatchOS, dll ...)

Masalah desain: NavigationLinks di-hardcode ke dalam View.

NavigationLink(destination: MyCustomView(item: item))

Tetapi jika tampilan yang mengandung ini NavigationLinkharus dapat digunakan kembali saya tidak dapat membuat hardcode tujuan. Harus ada mekanisme yang menyediakan tujuan. Saya bertanya di sini dan mendapat jawaban yang cukup baik, tetapi masih belum jawaban lengkap:

SwiftUI MVVM Coordinator / Router / NavigationLink

Idenya adalah untuk menyuntikkan Tautan Tujuan ke tampilan yang dapat digunakan kembali. Secara umum ide ini bekerja tetapi sayangnya ini tidak skala ke Aplikasi Produksi nyata. Segera setelah saya memiliki beberapa layar yang dapat digunakan kembali, saya mengalami masalah logis bahwa satu tampilan yang dapat digunakan kembali ( ViewA) membutuhkan tampilan-tujuan yang telah dikonfigurasikan sebelumnya ( ViewB). Tetapi bagaimana jika ViewBjuga membutuhkan view-destination yang telah dikonfigurasi sebelumnya ViewC? Saya akan perlu membuat ViewBsudah sedemikian rupa sehingga ViewCdisuntikkan sudah di ViewBsebelum saya menyuntikkan ViewBke dalam ViewA. Dan seterusnya .... tetapi karena data yang pada saat itu harus dilewati tidak tersedia, keseluruhan konstruksinya gagal.

Gagasan lain yang saya miliki adalah menggunakan Environmentmekanisme injeksi ketergantungan untuk menyuntikkan tujuan NavigationLink. Tapi saya pikir ini harus dianggap kurang lebih sebagai peretasan dan bukan solusi yang dapat diskalakan untuk Aplikasi besar. Kami pada akhirnya akan menggunakan Lingkungan pada dasarnya untuk semuanya. Tetapi karena Lingkungan juga dapat digunakan hanya di dalam View (tidak di Koordinator atau ViewModels terpisah) ini lagi akan membuat konstruksi aneh menurut saya.

Seperti logika bisnis (misalnya kode model tampilan) dan tampilan harus dipisahkan juga navigasi dan tampilan harus dipisahkan (misalnya pola Koordinator) Di UIKitdalamnya dimungkinkan karena kami mengakses ke UIViewControllerdan di UINavigationControllerbelakang tampilan. UIKit'sMVC sudah memiliki masalah yang membuat begitu banyak konsep sehingga menjadi nama yang menyenangkan "Massive-View-Controller" bukannya "Model-View-Controller". Sekarang masalah yang sama terus berlanjut SwiftUItetapi bahkan lebih buruk lagi menurut saya. Navigasi dan Tampilan sangat digabungkan dan tidak dapat dipisahkan. Karenanya tidak mungkin untuk melakukan tampilan yang dapat digunakan kembali jika mengandung navigasi. Itu mungkin untuk menyelesaikan ini, UIKittetapi sekarang saya tidak dapat melihat solusi yang masuk akalSwiftUI. Sayangnya Apple tidak memberikan penjelasan kepada kami bagaimana menyelesaikan masalah arsitektur seperti itu. Kami hanya mendapat beberapa Aplikasi sampel kecil.

Saya ingin terbukti salah. Tolong tunjukkan saya pola desain App bersih yang memecahkan ini untuk Aplikasi siap produksi besar.

Terima kasih sebelumnya.


Pembaruan: karunia ini akan berakhir dalam beberapa menit dan sayangnya masih tidak ada yang bisa memberikan contoh yang berfungsi. Tetapi saya akan memulai karunia baru untuk menyelesaikan masalah ini jika saya tidak dapat menemukan solusi lain dan menautkannya di sini. Terima kasih untuk semua untuk Kontribusi mereka yang luar biasa!

Darko
sumber
1
Sepakat! Saya membuat permintaan untuk ini di "Asisten Umpan Balik" beberapa bulan yang lalu, belum ada tanggapan: gist.github.com/Sajjon/b7edb4cc11bcb6462f4e28dc170be245
Sajjon
@Sajjon Terima kasih! Saya bermaksud menulis Apple juga, mari kita lihat apakah saya mendapat respons.
Darko
1
A menulis surat kepada Apple mengenai hal ini. Mari kita lihat apakah kita mendapat tanggapan.
Darko
1
Bagus! Ini akan menjadi hadiah terbaik selama WWDC!
Sajjon

Jawaban:

10

Penutupan adalah semua yang Anda butuhkan!

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

Saya menulis posting tentang mengganti pola delegasi di SwiftUI dengan penutupan. https://swiftwithmajid.com/2019/11/06/the-power-of-closures-in-swiftui/

Mekid
sumber
Penutupan itu ide yang bagus, terima kasih! Tetapi bagaimana hal itu terlihat dalam hierarki pandangan yang mendalam? Bayangkan saya memiliki NavigationView yang masuk 10 level lebih dalam, detail, ke detail, ke detail, dll ...
Darko
Saya ingin mengundang Anda untuk menunjukkan beberapa contoh kode sederhana hanya sedalam tiga level.
Darko
7

Ide saya akan menjadi kombinasi Coordinatordan Delegatepola. Pertama, buat Coordinatorkelas:


struct Coordinator {
    let window: UIWindow

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

Adaptasi SceneDelegateuntuk menggunakan 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()
        }
    }

Di dalam ContentView, kami memiliki ini:


struct ContentView: View {
    var delegate: ContentViewDelegate?

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

Kita dapat mendefinisikan ContenViewDelegateprotokol seperti ini:

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

Di mana Itemhanya sebuah struct yang dapat diidentifikasi, bisa berupa apa saja (misalnya id dari beberapa elemen seperti di TableViewdalam UIKit)

Langkah selanjutnya adalah mengadopsi protokol ini Coordinatordan cukup menyampaikan pandangan yang ingin Anda sajikan:

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

Sejauh ini ini berfungsi dengan baik di aplikasi saya. Saya harap ini membantu.

Nikola Matijevic
sumber
Terima kasih untuk kode sampel. Saya ingin mengundang Anda untuk berubah Text("Returned Destination1")ke sesuatu seperti MyCustomView(item: ItemType, destinationView: View). Sehingga MyCustomViewjuga perlu beberapa data dan tujuan disuntikkan. Bagaimana Anda menyelesaikannya?
Darko
Anda mengalami masalah bersarang yang saya jelaskan di posting saya. Tolong koreksi saya jika saya salah. Pada dasarnya pendekatan ini berfungsi jika Anda memiliki satu tampilan yang dapat digunakan kembali dan tampilan yang dapat digunakan kembali itu tidak mengandung tampilan yang dapat digunakan kembali dengan NavigationLink. Yang merupakan kasus penggunaan yang cukup sederhana tetapi tidak skala ke Aplikasi besar. (di mana hampir setiap tampilan dapat digunakan kembali)
Darko
Ini sangat tergantung pada bagaimana Anda mengelola dependensi aplikasi Anda dan alurnya. Jika Anda memiliki dependensi di satu tempat, seperti seharusnya Anda IMO (juga dikenal sebagai root komposisi), Anda seharusnya tidak mengalami masalah ini.
Nikola Matijevic
Apa yang berhasil bagi saya adalah mendefinisikan semua dependensi Anda untuk tampilan sebagai protokol. Tambahkan kesesuaian dengan protokol di root komposisi. Berikan dependensi kepada koordinator. Suntikkan mereka dari koordinator. Secara teori, Anda harus memiliki lebih dari tiga parameter, jika dilakukan dengan benar, tidak pernah lebih dari dependenciesdan destination.
Nikola Matijevic
1
Saya ingin melihat contoh nyata. Seperti yang sudah saya sebutkan, mari kita mulai Text("Returned Destination1"). Bagaimana jika ini perlu a MyCustomView(item: ItemType, destinationView: View). Apa yang akan Anda suntikkan di sana? Saya memahami injeksi dependensi, kopling longgar melalui protokol, dan dependensi bersama dengan koordinator. Semua itu bukan masalah - itu adalah sarang yang dibutuhkan. Terima kasih.
Darko
2

Sesuatu yang terjadi pada saya adalah ketika Anda mengatakan:

Tetapi bagaimana jika ViewB juga membutuhkan ViewC destinasi-tujuan yang telah dikonfigurasi sebelumnya? Saya perlu membuat ViewB sedemikian rupa sehingga ViewC sudah disuntikkan di ViewB sebelum saya menyuntikkan ViewB ke dalam ViewA. Dan seterusnya .... tetapi karena data yang pada saat itu harus dilewati tidak tersedia, keseluruhan konstruksinya gagal.

itu tidak sepenuhnya benar. Daripada menyediakan tampilan, Anda dapat merancang komponen yang dapat digunakan kembali sehingga Anda memberikan penutupan yang memasok tampilan pada permintaan.

Dengan cara itu penutupan yang menghasilkan ViewB on demand dapat menyediakannya dengan penutupan yang menghasilkan ViewC on demand, tetapi konstruksi aktual dari pandangan dapat terjadi pada saat informasi kontekstual yang Anda butuhkan tersedia.

Sam Deane
sumber
Tetapi bagaimana perbedaan penciptaan "penutupan pohon" seperti itu dari pandangan yang sebenarnya? Masalah penyediaan barang akan diselesaikan, tetapi tidak perlu bersarang. Saya membuat penutupan yang membuat tampilan - ok. Tetapi dalam penutupan itu saya sudah harus menyediakan penciptaan penutupan berikutnya. Dan yang terakhir berikutnya. Dll ... tapi mungkin saya salah paham dengan Anda. Beberapa contoh kode akan membantu. Terima kasih.
Darko
2

Ini adalah contoh menyenangkan dari menelusuri tanpa batas dan mengubah data Anda untuk tampilan detail selanjutnya secara terprogram

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())
    }
}
MScottWaller
sumber
-> beberapa View memaksa Anda untuk selalu mengembalikan hanya satu jenis View.
Darko
Injeksi ketergantungan dengan EnvironmentObject memecahkan satu bagian dari masalah. Tetapi: haruskah sesuatu yang penting dan penting dalam kerangka UI harus sedemikian rumit ...?
Darko
Maksud saya - jika injeksi ketergantungan adalah satu - satunya solusi untuk ini maka saya dengan enggan akan menerimanya. Tapi ini benar-benar akan berbau ...
Darko
1
Saya tidak mengerti mengapa Anda tidak bisa menggunakan ini dengan contoh kerangka kerja Anda. Jika Anda berbicara tentang kerangka kerja yang meng-view pandangan yang tidak diketahui saya bayangkan itu hanya bisa mengembalikan beberapa View. Saya juga tidak akan terkejut jika AnyView di dalam NavigationLink sebenarnya tidak terlalu besar sebagai hit karena tampilan induk sepenuhnya dipisahkan dari tata letak aktual anak. Tapi saya bukan ahli, itu harus diuji. Alih-alih meminta semua orang untuk kode sampel di mana mereka tidak dapat sepenuhnya memahami persyaratan Anda mengapa Anda tidak menulis sampel UIKit dan meminta terjemahan?
jasongregori
1
Desain ini pada dasarnya adalah bagaimana aplikasi (UIKit) yang saya gunakan bekerja. Model dihasilkan yang menghubungkan ke model lain. Sistem pusat menentukan vc apa yang harus dimuat untuk model itu dan kemudian induk vc mendorongnya ke stack.
jasongregori
2

Saya sedang menulis seri posting blog tentang cara membuat pendekatan MVP + Coordinators di SwiftUI yang mungkin berguna:

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

Proyek lengkap tersedia di Github: https://github.com/Lascorbe/SwiftUI-MVP-Coordinator

Saya mencoba melakukannya seolah-olah itu akan menjadi aplikasi besar dalam hal skalabilitas. Saya pikir saya sudah menyelesaikan masalah navigasi, tapi saya masih harus melihat bagaimana melakukan deep linking, yang saat ini saya kerjakan. Saya harap ini membantu.

Luis Ascorbe
sumber
Wow, bagus sekali, terima kasih! Anda melakukan pekerjaan yang cukup baik dalam mengimplementasikan Koordinator di SwiftUI. Gagasan untuk membuat NavigationViewtampilan root sangat fantastis. Sejauh ini, ini adalah implementasi Koordinator SwiftUI paling canggih yang saya lihat sejauh ini.
Darko
Saya ingin memberikan hadiah kepada Anda hanya karena solusi Koordinator Anda sangat bagus. Satu-satunya masalah yang saya miliki - itu tidak benar-benar mengatasi masalah yang saya jelaskan. Itu decouple NavigationLinktetapi melakukannya dengan memperkenalkan ketergantungan baru digabungkan. Dalam MasterViewcontoh Anda tidak bergantung pada NavigationButton. Bayangkan menempatkan MasterViewdalam Paket Swift - itu tidak akan dikompilasi lagi karena jenisnya NavigationButtontidak diketahui. Juga saya tidak melihat bagaimana masalah reusable bersarang Viewsakan diselesaikan olehnya?
Darko
Saya akan senang menjadi salah, dan jika saya kemudian tolong jelaskan kepada saya. Meskipun hadiahnya habis dalam beberapa menit, saya harap saya bisa memberi Anda poin. (tidak pernah melakukan karunia sebelumnya, tapi saya pikir saya bisa membuat pertanyaan lanjutan dengan yang baru?)
Darko
1

Ini adalah jawaban yang benar-benar luar biasa, jadi mungkin akan berubah menjadi omong kosong, tetapi saya akan tergoda untuk menggunakan pendekatan hybrid.

Gunakan lingkungan untuk melewati objek koordinator tunggal - sebut saja NavigationCoordinator.

Berikan pandangan yang dapat digunakan kembali Anda semacam pengidentifikasi yang diatur secara dinamis. Identifier ini memberikan informasi semantik yang sesuai dengan kasus penggunaan aktual dan hierarki navigasi aplikasi klien.

Mintalah pandangan yang dapat digunakan kembali meminta Navigator untuk tampilan tujuan, melewati pengidentifikasi mereka dan pengidentifikasi jenis tampilan yang mereka navigasikan.

Ini meninggalkan NavigationCoordinator sebagai titik injeksi tunggal, dan itu adalah objek non-view yang dapat diakses di luar hierarki tampilan.

Selama penyetelan, Anda dapat mendaftarkan kelas tampilan kanan agar kembali, menggunakan semacam pencocokan dengan pengidentifikasi yang diteruskan saat runtime. Sesuatu yang sederhana seperti mencocokkan dengan pengidentifikasi tujuan mungkin berfungsi dalam beberapa kasus. Atau cocok dengan sepasang pengidentifikasi host dan tujuan.

Dalam kasus yang lebih kompleks, Anda dapat menulis pengontrol khusus yang memperhitungkan informasi spesifik aplikasi lainnya.

Karena itu disuntikkan melalui lingkungan, tampilan apa pun dapat mengesampingkan NavigationCoordinator default di setiap titik dan menyediakan yang berbeda untuk subview-nya.

Sam Deane
sumber