Adapter votre solution commerciale existante pour SwiftUI. Partie 3. Travailler avec l'architecture

Bonne journée à tous! Avec vous, moi, Anna Zharkova, l'un des principaux développeurs mobiles d' Usetech, nous

continuons de démonter les subtilités de SwiftUI. Les parties précédentes se trouvent sur les liens:

partie 1
partie 2

Aujourd'hui, nous allons parler des fonctionnalités de l'architecture, et comment transférer et intégrer la logique métier existante dans l'application SwiftUI.

Le flux de données standard dans SwiftUI est basé sur l'interaction de View et d'un certain modèle contenant des propriétés et des variables d'état, ou qui est une telle variable d'état. Par conséquent, il est logique que MVVM soit le partenaire architectural recommandé pour les applications SwiftUI. Apple suggère de l'utiliser en conjonction avec le framework Combine, qui introduit Api SwiftUI déclaratif pour le traitement des valeurs dans le temps. ViewModel implémente le protocole ObservableObject et se connecte en tant qu'ObservedObject à une vue spécifique.



Les propriétés de modèle modifiables sont déclarées comme @Published.

class NewsItemModel: ObservableObject {
    @Published var title: String = ""
    @Published var description: String = ""
    @Published var image: String = ""
    @Published var dateFormatted: String = ""
}

Comme dans le MVVM classique, ViewModel communique avec le modèle de données (c'est-à-dire la logique métier) et transfère les données sous une forme ou une autre vue.

struct NewsItemContentView: View {
    @ObservedObject var moder: NewsItemModel
    
    init(model: NewsItemModel) {
        self.model = model 
    }
    //... - 
}

MVVM, comme presque tout autre modèle, a tendance à la congestion et à la
redondance. Le ViewModel surchargé dépend toujours de la façon dont la logique métier est mise en évidence et abstraite. La charge de la vue est déterminée par la complexité de la dépendance des éléments à l'égard des variables d'état et des transitions vers une autre vue.

Dans SwiftUI, ce qui est ajouté, c'est que View est une structure, pas une classe , et ne prend donc pas en charge l'héritage, ce qui force la duplication de code.
Si, dans les petites applications, ce n'est pas critique, alors avec la croissance des fonctionnalités et la complexité de la logique, la surcharge devient critique et une grande quantité de copier-coller inhibe.

Essayons d'utiliser l'approche du code propre et de l'architecture propre dans ce cas. Nous ne pouvons pas complètement abandonner MVVM, après tout, DataFlow SwiftUI est construit dessus, mais c'est un peu à reconstruire.

Attention!

Si vous êtes allergique aux articles sur l'architecture et que le code Clean se retourne d'une phrase, faites défiler quelques paragraphes vers le bas.
Ce n'est pas exactement du code propre d'Oncle Bob!


Oui, nous ne prendrons pas le code propre de l'oncle Bob dans sa forme la plus pure. Quant à moi, il y a une ingénierie excessive. Nous ne prendrons qu'une idée.

L'idée principale du code propre est de créer le code le plus lisible, qui peut ensuite être développé et modifié sans douleur.

Il existe de nombreux principes de développement logiciel auxquels il est recommandé de se conformer.



Beaucoup de gens les connaissent, mais tout le monde ne les aime pas et tout le monde ne les utilise pas. Il s'agit d'un sujet distinct pour holivar.

Pour garantir la pureté du code, il faut au moins diviser le code en couches et modules fonctionnels, utiliser la solution générale des problèmes et mettre en œuvre l'abstraction de l'interaction entre les composants. Et au moins, vous devez séparer le code de l'interface utilisateur de la soi-disant logique métier.

Quel que soit le modèle architectural sélectionné, la logique de travail avec la base de données et le réseau, le traitement et le stockage des données sont séparés de l'interface utilisateur et des modules de l'application elle-même. Dans le même temps, les modules fonctionnent avec des implémentations de services ou de stockages, qui à leur tour accèdent au service général des requêtes réseau ou au stockage général des données. L'initialisation des variables par lesquelles vous pouvez accéder à l'un ou l'autre service est effectuée dans un certain conteneur général, auquel le module d'application (logique métier du module) accède finalement.



Si nous avons sélectionné et abstrait la logique métier, nous pouvons organiser l'interaction entre les composants des modules comme nous le souhaitons.

En principe, tous les modèles existants d'applications iOS fonctionnent sur le même principe.



Il y a toujours une logique métier, il y a des données. Il existe également un gestionnaire d'appels, qui est responsable de la présentation et de la transformation des données pour la sortie et où les données converties sont sorties. La seule différence est la façon dont les rôles sont répartis entre les composants.


Parce que nous nous efforçons de rendre l'application lisible, de simplifier les changements actuels et futurs, il est logique de séparer tous ces rôles. Notre logique métier a déjà été mise en avant, les données sont toujours séparées. Restez répartiteur, présentateur et vue. En conséquence, nous obtenons une architecture composée de View-Interactor-Presenter, dans laquelle l'interacteur interagit avec les services de logique métier, le présentateur convertit les données et les donne comme une sorte de ViewModel à notre View. Dans le bon sens, la navigation et la configuration sont également supprimées de View dans des composants séparés.



Nous obtenons l'architecture VIP + R avec la division des rôles controversés en différents composants.

Essayons de regarder un exemple. Nous avons une petite application d'agrégation de nouvelles
écrite en SwiftUI et MVVM.



L'application dispose de 3 écrans distincts avec sa propre logique, soit 3 modules:


  • module de liste de nouvelles;
  • module d'écran d'actualités;
  • module de recherche d'actualités.

Chacun des modules se compose d'un ViewModel, qui interagit avec la logique métier sélectionnée, et d'une View, qui affiche ce que le ViewModel lui diffuse.



Nous nous efforçons de nous assurer que ViewModel ne s'occupe que de stocker des données prêtes à être affichées. Maintenant, il est engagé à la fois dans l'accès aux services et dans le traitement des résultats.

Nous transférons ces rôles au présentateur et à l'interacteur, que nous configurons pour chaque
module.



L'interacteur transmet les données reçues du service au présentateur, qui remplit le ViewModel existant lié à la vue avec les données préparées. En principe, en ce qui concerne la séparation de la logique métier du module, tout est simple. 


Allez maintenant dans View. Essayons de gérer la duplication de code forcée. Si nous avons affaire à une sorte de contrôle, ce peut être ses styles ou ses paramètres. Si nous parlons de la vue à l'écran, alors ceci:

  • Styles d'écran
  • éléments d'interface utilisateur communs (LoadingView);
  • alertes d'information;
  • quelques méthodes générales.

Nous ne pouvons pas utiliser l'héritage, mais nous pouvons tout à fait utiliser la composition . C'est par ce principe que toutes les vues personnalisées dans SwiftUI sont créées.

Ainsi, nous créons un conteneur View, dans lequel nous transférons toute la même logique, et passons notre vue à l'écran à l'initialiseur de conteneur, puis nous l'utilisons comme une vue de contenu à l'intérieur du corps.

struct ContainerView<Content>: IContainer, View where Content: View {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    public init(content: Content) {
        self.content = content
    }

    var body : some View {
        ZStack {
            content
            if (self.containerModel.isLoading) {
                LoaderView()
            }
        }.alert(isPresented: $containerModel.hasError){
            Alert(title: Text(""), message: Text(containerModel.errorText),
                 dismissButton: .default(Text("OK")){
                self.containerModel.errorShown()
                })
        }
    }

La vue à l'écran est intégrée dans la ZStack à l'intérieur du corps ContainerView, qui contient également du code pour afficher la LoadingView et du code pour afficher une alerte d'information.

Nous avons également besoin de notre ContainerView pour recevoir un signal du ViewModel de la vue interne et mettre à jour son état. Nous ne pouvons pas nous abonner via @Observed au même modèle
que la vue interne, car nous ferons glisser ses signaux.



Par conséquent, nous établissons une communication avec lui via le modèle délégué, et pour l'état actuel du conteneur, nous utilisons son propre ContainerModel.

class ContainerModel:ObservableObject {
    @Published var hasError: Bool = false
    @Published var errorText: String = ""
    @Published var isLoading: Bool = false
    
    func setupError(error: String){
     //....
       }
    
    func errorShown() {
     //...
    }
    
    func showLoading() {
        self.isLoading = true
    }
    
    func hideLoading() {
        self.isLoading = false
    }
}

ContainerView implémente le protocole IContainer; une référence d'instance est affectée au modèle View intégré.

protocol  IContainer {
    func showError(error: String)
    
    func showLoading()
    
    func hideLoading()
}

struct ContainerView<Content>: IContainer, View where Content: View&IModelView {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    public init(content: Content) {
        self.content = content
        self.content.viewModel?.listener = self
    }
    //- 
}

View implémente le protocole IModelView pour encapsuler l'accès au modèle et unifier une logique. Des modèles dans le même but implémentent le protocole IModel:

protocol IModelView {
    var viewModel: IModel? {get}
}

protocol  IModel:class {
   //....
    var listener:IContainer? {get set}
}

Ensuite, déjà dans ce modèle, si nécessaire, la méthode déléguée est appelée, par exemple, pour afficher une alerte avec une erreur dans laquelle la variable d'état du modèle de conteneur change.

struct ContainerView<Content>: IContainer, View where Content: View&IModelView {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    //- 
    func showError(error: String) {
        self.containerModel.setupError(error: error)
    }
    
    func showLoading() {
        self.containerModel.showLoading()
    }
    
    func hideLoading() {
        self.containerModel.hideLoading()
    }
}

Nous pouvons maintenant unifier le travail de la vue en passant au travail via ContainerView.
Cela facilitera grandement notre vie lorsque nous travaillerons avec la configuration des modules et la navigation suivants.
Comment configurer la navigation dans SwiftUI et faire une configuration propre, nous parlerons dans la partie suivante .

Vous pouvez trouver le code source de l'exemple ici .

All Articles