Adapter votre solution commerciale existante pour SwiftUI. Partie 4. Navigation et configuration

Bonne journée à tous! Avec vous, moi, Anna Zharkova, l'un des principaux développeurs mobiles d' Usetech. Parlons
maintenant d'un autre point intéressant dans SwiftUI, la navigation.

Si vous avez manqué les articles précédents de la série, vous pouvez les lire ici:

partie 1
partie 2
partie 3

Avec le changement dans la description de la partie visuelle et le passage à la syntaxe déclarative, le contrôle de navigation dans l'application SwiftUI a également changé. L'utilisation de l'UIViewContoller est directement refusée; l'UINavigationController n'est pas directement utilisé. Il est remplacé par NavigationView.

@available(iOS 13.0, OSX 10.15, tvOS 13.0, *)
@available(watchOS, unavailable)
public struct NavigationView<Content> : View where Content : View {

    public init(@ViewBuilder content: () -> Content)

    //....
}

Essentiellement un wrapper sur l'UINavigationController et ses fonctionnalités.


Le principal mécanisme de transition est NavigationLink (un analogue de segue), qui est défini immédiatement dans le code de visualisation du corps.

public struct NavigationLink<Label, Destination> : View where Label : View, 
                                                                                           Destination : View {
.
    public init(destination: Destination, @ViewBuilder label: () -> Label)

    public init(destination: Destination, isActive: Binding<Bool>, 
                   @ViewBuilder label: () -> Label)

    public init<V>(destination: Destination, tag: V, selection: Binding<V?>,
                          @ViewBuilder label: () -> Label) where V : Hashable
//....
}

Lors de la création d'un NavigationLink, il indique la vue vers laquelle la transition est effectuée, ainsi que la vue que le NavigationLink encapsule, c'est-à-dire qu'en interagissant avec lui, le NavigationLink est activé. Plus d'informations sur les façons possibles d'initialiser NavigationLink dans la documentation Apple.

Cependant, il convient de garder à l' esprit qu'il n'y a pas d'accès direct à la pile de vues en raison de l'encapsulation, la navigation est programmée uniquement vers l'avant, le retour n'est possible que d'un niveau vers l'arrière, puis via le code encapsulé pour le bouton "Retour"



Toujours dans SwiftUI, il n'y a pas de navigation logicielle dynamique. Si la transition n'est pas liée à un événement déclencheur, par exemple, en appuyant sur un bouton, mais suit à la suite d'une sorte de logique, cela ne peut tout simplement pas être fait. La transition vers la vue suivante est nécessairement liée au mécanisme NavigationLink, qui est défini de manière déclarative immédiatement lors de la description de la vue qui les contient. Tout.

Si notre écran doit contenir une transition vers de nombreux écrans différents, alors le code devient lourd:

  NavigationView{
            NavigationLink(destination: ProfileView(), isActive: self.$isProfile) {
                Text("Profile")
            }
            NavigationLink(destination: Settings(), isActive: self.$isSettings) {
                           Text("Settings")
                       }
            NavigationLink(destination: Favorite(), isActive: self.$isFavorite) {
                Text("Favorite")
            }
            NavigationLink(destination: Login(), isActive: self.$isLogin) {
                Text("Login")
            }
            NavigationLink(destination: Search(), isActive: self.$isSearch) {
                Text("Search")
            }
}

Nous pouvons gérer les liens de plusieurs manières:
- Contrôle d'activité de NavigationLink via la propriété @Binding

 NavigationLink(destination: ProfileView(), isActive: self.$isProfile) {
                Text("Profile")
            }

- contrôle de la création de liens via une condition (variables d'état)

       if self.isProfile {
            NavigationLink(destination: ProfileView()) {
                Text("Profile")
            }
}

La première méthode nous ajoute le travail de surveillance de l'état des variables de contrôle.
Si nous prévoyons de naviguer plus d'un niveau plus loin, alors c'est une tâche très difficile.

Dans le cas de l'écran de la liste des éléments similaires, tout semble compact:

 NavigationView{
        List(model.data) { item in
            NavigationLink(destination: NewsItemView(item:item)) {
            NewsItemRow(data: item)
            }
        }

Le problème le plus grave de NavigationLink, à mon avis, est que tous les liens View spécifiés ne sont pas paresseux. Ils sont créés non pas au moment où le lien est déclenché, mais au moment de la création. Si nous avons une liste de nombreux éléments ou transitions vers de nombreux contenus lourds View différents, cela n'affecte pas les performances de notre application de la meilleure façon. Si nous avons encore ViewModel attaché à ces vues avec logique, dans la mise en œuvre de laquelle la vue du cycle de vie n'est pas prise en compte ou n'est pas prise en compte correctement, alors la situation devient très difficile.

Par exemple, nous avons une liste de nouvelles avec des éléments du même type. Nous ne sommes encore jamais allés sur un seul écran d'une seule news, et les modèles sont déjà en mémoire:



que pouvons-nous faire dans ce cas pour nous faciliter la vie?

Tout d'abord, rappelez-vous que View n'existe pas dans le vide, mais est rendu dans UIHostingController.

open class UIHostingController<Content> : UIViewController where Content : View {

    public init(rootView: Content)

    public var rootView: Content
//...
}

Et voici l'UIViewController. Nous pouvons donc faire ce qui suit. Nous transférerons toute la responsabilité de la transition vers la prochaine vue à l'intérieur du nouveau UIHostingController au contrôleur de la vue actuelle. Créons des modules de navigation et de configuration que nous appellerons depuis notre vue.

Le navigateur travaillant avec UIViewController ressemblera à ceci:

class Navigator {
    private init(){}
    
    static let shared = Navigator()
    
    private weak var view: UIViewController?
    
    internal weak var nc: UINavigationController?
        
   func setup(view: UIViewController) {
        self.view = view
    }
     
  internal func open<Content:View>(screen: Content.Type, _ data: Any? = nil) {
     if let vc = ModuleConfig.shared.config(screen: screen)?
        .createScreen(data) {
        self.nc?.pushViewController(vc, animated: true)
        }
   }

Par le même principe, nous allons créer une fabrique de configurateurs, qui nous donnera l'implémentation du configurateur d'un module spécifique:

protocol IConfugator: class {
    func createScreen(_ data: Any?)->UIViewController
}

class ModuleConfig{
    private init(){}
    static let shared = ModuleConfig()
    
    func config<Content:View>(screen: Content.Type)->IConfugator? {
        if screen == NewsListView.self {
            return NewsListConfigurator.shared
        }
      // -
        return nil
    }
}

Le navigateur, par type d'écran, demande le configurateur d'un module particulier, lui transfère toutes les informations nécessaires.

class NewsListConfigurator: IConfugator {
    static let shared = NewsListConfigurator()
    
    func createScreen(_ data: Any?) -> UIViewController {
         var view = NewsListView()
        let presenter = NewsListPresenter()
         let interactor = NewsListInteractor()
        
        interactor.output = presenter
        presenter.output = view
        view.output = interactor
        
        let vc = UIHostingController<ContainerView<NewsListView>>
                    (rootView: ContainerView(content: view))
        return vc
    }
}

Le configurateur donne le UIViewController, qui est le navigateur et pousse le UINavigationController sur la pile partagée.



Remplacez NavigationLink dans le code par un appel à Navigator. Comme déclencheur, nous aurons un événement de clic sur un élément de la liste:

  List(model.data) { item in
            NewsItemRow(data: item)
                .onTapGesture {
                Navigator.shared.open(screen: NewsItemView.self, item)
            }
        }

Rien ne nous empêche d'invoquer Navigator dans une méthode View de la même manière. Pas seulement à l'intérieur du corps.

Outre le fait que le code est devenu nettement plus propre, nous avons également déchargé la mémoire. Après tout, avec cette approche, la vue sera créée uniquement lorsqu'elle sera appelée.



Maintenant, notre application SwiftUI est plus facile à développer et à modifier. Le code est propre et beau.
Vous pouvez trouver l'exemple de code ici .

La prochaine fois, nous parlerons de la mise en œuvre plus approfondie de Combine.

All Articles