SwiftUI - görünüme sabit kodlanmış navigasyon nasıl önlenir?


33

Daha büyük, üretime hazır SwiftUI Uygulaması için mimariyi yapmaya çalışıyorum. Her zaman SwiftUI'deki büyük bir tasarım kusuruna işaret eden aynı soruna koşuyorum.

Hala kimse bana tam çalışmaya ve üretime hazır bir cevap veremedi.

SwiftUIGezinme içeren yeniden kullanılabilir Görünümler nasıl yapılır ?

Gibi SwiftUI NavigationLinkkuvvetli görünümüne bağlı bu daha büyük Uygulamalarda da ölçekler olduğunu basitçe şekilde mümkün değildir. NavigationLinkBu küçük örnek Uygulamalar çalışır, evet - ama bir App birçok Görünümler yeniden kullanmak istediğiniz anda değil. Ve belki de modül sınırları üzerinden tekrar kullanılabilir. (gibi: iOS, WatchOS vb. görünümü yeniden kullanma ...)

Tasarım sorunu: NavigationLinks Görünüm'e sabit kodlanmıştır.

NavigationLink(destination: MyCustomView(item: item))

Ancak bunu içeren görünüm NavigationLinkyeniden kullanılabilir olması gerekiyorsa ben hedef kodlayamaz . Hedefi sağlayan bir mekanizma olmalıdır. Bunu burada sordum ve oldukça iyi bir cevap aldım, ama yine de tam cevap değil:

SwiftUI MVVM Koordinatörü / Yönlendirici / NavigationLink

Fikir, Hedef Bağlantıları yeniden kullanılabilir görünüme enjekte etmekti. Genellikle fikir işe yarıyor ama maalesef bu gerçek Üretim Uygulamalarına göre ölçeklenmiyor. Birden çok yeniden kullanılabilir ekranım olur olmaz, yeniden kullanılabilir bir görünümün ( ViewA) önceden yapılandırılmış bir görüntüleme hedefine ( ViewB) ihtiyaç duyduğu mantıksal sorunla karşılaşıyorum . Ancak ViewBönceden yapılandırılmış bir görüntüleme hedefine de ihtiyaç duyulursa ne olur ViewC? Ben oluşturmak gerekir ViewBşekilde zaten ViewCzaten enjekte edilir ViewBBen enjekte önce ViewBiçine ViewA. Ve böylece .... ama o zaman iletilmesi gereken veriler mevcut olmadığından, tüm yapı başarısız olur.

Sahip olduğum başka bir fikir, Environmenthedefleri enjekte etmek için bağımlılık enjeksiyon mekanizması olarak kullanmaktı NavigationLink. Ancak bunun büyük uygulamalar için ölçeklenebilir bir çözüm değil, az çok bir kesmek olarak düşünülmesi gerektiğini düşünüyorum. Sonuçta Ortamı temelde her şey için kullanacağız. Ancak Çevre aynı zamanda yalnızca View'ların içinde (ayrı Koordinatörlerde veya ViewModellerde değil) kullanılabileceğinden, bu bana göre garip yapılar yaratacaktır.

İş mantığı (örn görmek model kodu) ve görünümü gibi ayrıca navigasyon ayrılması gereken ve de (örneğin Koordinatör deseni) ayrılacak zorunda görüntülemek UIKitbiz erişim çünkü bu mümkündür UIViewControllerve UINavigationControllergörünümü arkasında. UIKit'sMVC zaten "Model-View-Controller" yerine "Massive-View-Controller" eğlence adı haline gelen birçok kavramı ezdi. Şimdi benzer bir sorun devam ediyor SwiftUIama bence daha da kötü. Gezinme ve Görünümler güçlü bir şekilde birleştirilmiştir ve ayrıştırılamaz. Bu nedenle, gezinme içeriyorsa yeniden kullanılabilir görünümler yapmak mümkün değildir. Bunu çözmek mümkün oldu UIKitama şimdi aklı başında bir çözüm göremiyorumSwiftUI. Ne yazık ki Apple bize böyle mimari sorunların nasıl çözüleceğine dair bir açıklama yapmadı. Sadece küçük örnek uygulamalar aldık.

Yanlış kanıtlanmayı çok isterim. Lütfen büyük üretime hazır Uygulamalar için bunu çözen temiz bir Uygulama tasarım deseni gösterin.

Şimdiden teşekkürler.


Güncelleme: Bu ödül birkaç dakika içinde sona erecek ve maalesef hiç kimse çalışan bir örnek sağlayamadı. Ama başka bir çözüm bulamaz ve buraya bağlarsam bu sorunu çözmek için yeni bir ödül başlatacağım. Katkılarından dolayı herkese teşekkürler!


1
Kabul! Ben “Geri Bildirim Yardımcısı” bunun için bir istek yarattı birkaç ay önce, henüz hiçbir yanıt: gist.github.com/Sajjon/b7edb4cc11bcb6462f4e28dc170be245
Sajjon

@Sajjon Teşekkürler! Apple'ı da yazmayı düşünüyorum, bir yanıt alıp almadığımı görelim.
Darko

1
A, Apple'a bununla ilgili bir mektup yazdı. Bakalım bir cevap verelim mi?
Darko

1
Güzel! WWDC sırasında bugüne kadarki en iyi hediye!
Sajjon

Yanıtlar:


10

Kapatma tüm ihtiyacınız!

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'deki delege modelini kapaklarla değiştirme hakkında bir yazı yazdım. https://swiftwithmajid.com/2019/11/06/the-power-of-closures-in-swiftui/


Kapanış iyi bir fikir, teşekkürler! Fakat bu derin bir bakış hiyerarşisinde nasıl görünürdü? 10 seviye daha derine, detaylara, detaylara, detaylara vb
Darko

Sizi sadece üç seviyeli derinlikte basit bir örnek kod göstermeye davet etmek istiyorum.
Darko

7

Benim fikrim hemen hemen bir örüntü Coordinatorve Delegatekalıp kombinasyonu olurdu . İlk önce bir Coordinatorsınıf oluşturun :


struct Coordinator {
    let window: UIWindow

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

Şunu SceneDelegatekullanmak için uyarlayın 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()
        }
    }

İçinde ContentView, bu var:


struct ContentView: View {
    var delegate: ContentViewDelegate?

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

ContenViewDelegateProtokolü şöyle tanımlayabiliriz :

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

ItemSadece tanımlanabilir bir yapı nerede başka bir şey olabilir (örneğin TableViewUIKit'teki gibi bir öğenin kimliği )

Bir sonraki adım, bu protokolü benimsemek Coordinatorve sunmak istediğiniz görünümü iletmektir:

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

Bu şimdiye kadar uygulamalarımda güzel çalıştı. Umut ediyorum bu yardım eder.


Örnek kod için teşekkürler. Sizi böyle bir Text("Returned Destination1")şeye değişmeye davet etmek istiyorum MyCustomView(item: ItemType, destinationView: View). Bu yüzden MyCustomViewayrıca bazı veriler ve hedefe enjekte edilmesi gerekir. Bunu nasıl çözersin?
Darko

Benim yazımda açıkladığım yuvalama sorunuyla karşılaşıyorsunuz. Yanlışım varsa lütfen düzelt. Bir yeniden kullanılabilir görünüme sahip ve bu yeniden kullanılabilir görünümü eğer Temelde bu yaklaşım çalışır değil NavigationLink başka yeniden kullanılabilir bir görünüm içeriyor. Bu oldukça basit bir kullanım örneğidir, ancak büyük Uygulamalara göre ölçeklendirilmez. (neredeyse her görünüm yeniden kullanılabilir)
Darko

Bu, uygulama bağımlılıklarınızı ve akışlarını nasıl yönettiğinize bağlıdır. Tek bir yerde bağımlılıklar yaşıyorsanız, IMO (kompozisyon kökü olarak da bilinir) gibi, bu sorunla karşılaşmamalısınız.
Nikola Matijevic

Benim için işe yarayan bir görünüm için tüm bağımlılıklarınızı bir protokol olarak tanımlamaktır. Kompozisyon kökündeki protokole uygunluk ekleyin. Bağımlılıkları koordinatöre verin. Koordinatörden enjekte edin. Asla fazla doğru yapılırsa Teorik olarak, fazla üç parametreye ile bitirmek gerekir dependenciesve destination.
Nikola Matijevic

1
Somut bir örnek görmek isterim. Daha önce de belirttiğim gibi, başlayalım Text("Returned Destination1"). Ya bunun olması gerekiyorsa MyCustomView(item: ItemType, destinationView: View). Oraya ne enjekte edeceksin? Bağımlılık enjeksiyonunu, protokollerle gevşek bağlantıyı ve koordinatörlerle paylaşılan bağımlılıkları anlıyorum. Bütün bunlar sorun değil - gerekli yuvalama. Teşekkürler.
Darko

2

Benim başıma gelen bir şey şu ki:

Ancak, ViewB'nin önceden yapılandırılmış bir görüntüleme hedefi ViewC'ye de ihtiyacı varsa ne olur? ViewB içine ViewB enjekte önce ViewC zaten ViewB enjekte öyle bir şekilde ViewB oluşturmak gerekir. Ve böylece .... ama o zaman iletilmesi gereken veriler mevcut olmadığından, tüm yapı başarısız olur.

tam olarak doğru değil. Görünüm sağlamak yerine, yeniden kullanılabilir bileşenlerinizi, isteğe bağlı görünüm sağlayan kapaklar sağlayabilmeniz için tasarlayabilirsiniz.

Bu şekilde, isteğe bağlı olarak ViewB üreten kapatma, isteğe bağlı olarak ViewC üreten bir kapatma sağlayabilir, ancak görünümlerin gerçek yapısı, ihtiyacınız olan bağlamsal bilgilerin mevcut olduğu bir zamanda olabilir.


Peki bu tür “kapanma ağacının” yaratılması gerçek görüşlerden nasıl farklıdır? Sorunu sağlayan öğe çözüldü, ancak gerekli yuvalama değil. Bir görünüm oluşturan bir kapak oluşturuyorum - tamam. Ama bu kapanışta bir sonraki kapanışın yaratılmasını sağlamam gerekiyordu. Ve sonuncusunda bir sonraki. Vb ... ama belki seni yanlış anlıyorum. Bazı kod örnekleri yardımcı olabilir. Teşekkürler.
Darko

2

İşte sonsuz bir şekilde detaya inmenin ve bir sonraki ayrıntı görünümü için verilerinizi programlı olarak değiştirmenin eğlenceli bir örneği

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

-> bazı Görünümler sizi her zaman yalnızca bir tür Görünüm döndürmeye zorlar.
Darko

EnvironmentObject ile bağımlılık enjeksiyonu sorunun bir kısmını çözer. Ancak: UI çerçevesinde önemli ve önemli bir şey bu kadar karmaşık olmalı mı?
Darko

Yani - bağımlılık enjeksiyonu tek çözümse, o zaman isteksizce kabul ederdim. Ama bu gerçekten kokuyor ...
Darko

1
Bunu çerçeve örneğinizle neden kullanamadığınızı anlamıyorum. Bilinmeyen bir görünüm sağlayan bir çerçeveden bahsediyorsanız, bunun yalnızca bazı Görünümler döndürebileceğini hayal ediyorum. Üst görünüm, çocuğun gerçek düzeninden tamamen ayrıldığından, bir NavigationLink içindeki AnyView aslında bir ön vuruştan büyük değilse de şaşırmam. Yine de uzman değilim, test edilmesi gerekecekti. Herkesten gereksinimlerinizi tam olarak anlayamadıkları örnek kod istemek yerine neden bir UIKit örneği yazıp çeviri istemiyorsunuz?
jasongregori

1
Bu tasarım temelde üzerinde çalıştığım (UIKit) uygulaması nasıl çalışır. Diğer modellere bağlanan modeller üretilir. Merkezi bir sistem, o model için hangi vc'nin yüklenmesi gerektiğini belirler ve daha sonra üst vc onu yığının üzerine iter.
jasongregori

2

SwiftUI'de bir MVP + Koordinatörleri yaklaşımı oluşturmaya yönelik bir blog yayınları dizisi yazıyorum.

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

Projenin tamamını Github'da bulabilirsiniz: https://github.com/Lascorbe/SwiftUI-MVP-Coordinator

Bunu ölçeklenebilirlik açısından büyük bir uygulama gibi yapmaya çalışıyorum. Sanırım navigasyon problemini çözdüm, ancak hala derin bağlantıların nasıl yapıldığını görmem gerekiyor, şu anda üzerinde çalıştığım şey bu. Umut ediyorum bu yardım eder.


Vay canına, bu harika, teşekkürler! SwiftUI'de Koordinatörleri uygulamak konusunda oldukça iyi bir iş başardınız. NavigationViewKök görünümü yapma fikri harika. Bu şimdiye kadar gördüğüm en gelişmiş SwiftUI Koordinatörleri uygulaması.
Darko

Koordinatör çözümünüz gerçekten harika olduğu için size ödül vermek istiyorum. Sahip olduğum tek sorun - gerçekten anlattığım sorunu ele almıyor. Ayrılıyor NavigationLinkancak yeni bir bağımlı bağımlılık getirerek bunu yapıyor. MasterViewSizin örnekte bağlı değildir NavigationButton. MasterViewBir Swift Paketine yerleştirdiğinizi düşünün - tür NavigationButtonbilinmediği için artık derlenmeyecek . Ayrıca iç içe yeniden kullanılabilir sorunun Viewsonun tarafından nasıl çözüleceğini görmüyorum ?
Darko

Yanlış yaptığım için mutlu olurum ve eğer öyleyse lütfen bana açıkla. Ödül birkaç dakika içinde bitse de, umarım sana bir şekilde puan verebilirim. (daha önce hiç ödül vermedim, ama sanırım yeni bir soru ile takip sorusu oluşturabilir miyim?)
Darko

1

Bu tamamen başımın üstünde bir cevap, bu yüzden muhtemelen saçmalık olacak, ancak hibrit bir yaklaşım kullanmaya cazip geleceğim.

Tek bir koordinatör nesnesinden geçmek için ortamı kullanın - buna NavigationCoordinator diyelim.

Yeniden kullanılabilir görünümlerinize dinamik olarak ayarlanmış bir tür tanımlayıcı verin. Bu tanımlayıcı, istemci uygulamasının gerçek kullanım durumuna ve gezinme hiyerarşisine karşılık gelen anlamsal bilgiler verir.

Yeniden kullanılabilir görünümlerin, tanımlayıcılarını ve gittikleri görünüm türünün tanımlayıcısını ileterek hedef görünüm için NavigationCoordinator'ı sorgulamasını sağlayın.

Bu, NavigationCoordinator'ı tek bir enjeksiyon noktası olarak bırakır ve görünüm hiyerarşisinin dışında erişilebilen, görüntülenmeyen bir nesnedir.

Kurulum sırasında, çalışma zamanında geçirilen tanımlayıcılarla bir tür eşleme kullanarak geri dönmesi için doğru görünüm sınıflarını kaydedebilirsiniz. Bazı durumlarda hedef tanımlayıcıyla eşleştirmek kadar basit bir şey işe yarayabilir. Veya bir çift ana bilgisayar ve hedef tanımlayıcıyla eşleştirme.

Daha karmaşık durumlarda, uygulamaya özel diğer bilgileri dikkate alan özel bir denetleyici yazabilirsiniz.

Ortam yoluyla enjekte edildiğinden, herhangi bir görünüm varsayılan NavigationCoordinator'ı herhangi bir noktada geçersiz kılabilir ve alt görünümlerine farklı bir görünüm sağlayabilir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.