La bonne façon de partager l'état entre les contrôleurs de vue rapide

Ce que vous allez créer

Il y a quelques années, alors que j'étais encore employé dans un cabinet de conseil en téléphonie mobile, j'ai travaillé sur une application pour une grande banque d'investissement. Les grandes entreprises, en particulier les banques, ont généralement mis en place des processus garantissant la sécurité, la robustesse et la maintenance de leurs logiciels..

Une partie de ce processus a consisté à envoyer le code de l'application que j'ai écrite à un tiers pour révision. Cela ne m'a pas dérangé, car je pensais que mon code était impeccable et que la compagnie de révision dirait la même chose..

Quand leur réponse est revenue, le verdict était différent de ce que je pensais. Bien qu'ils aient déclaré que la qualité du code n'était pas mauvaise, ils ont souligné le fait que le code était difficile à maintenir et à tester (les tests unitaires n'étaient pas très populaires à l'époque dans le développement iOS)..

J'ai écarté leur jugement, pensant que mon code était génial et qu'il était impossible de l'améliorer. Ils doivent juste ne pas comprendre!

J'ai eu l'hybris typique du développeur: nous pensons souvent que ce que nous faisons est excellent et que d'autres ne l'obtiennent pas. 

Avec le recul, j'avais tort. Peu de temps après, j'ai commencé à lire certaines des meilleures pratiques. À partir de ce moment, les problèmes de mon code ont commencé à se poser comme un pouce endolori. J'ai réalisé que, comme beaucoup de développeurs iOS, j'avais succombé à quelques pièges classiques de mauvaises pratiques de codage..

Qu'est-ce que la plupart des développeurs iOS se trompent?

L'une des mauvaises pratiques de développement iOS les plus courantes survient lors du passage d'état entre les contrôleurs de vue d'une application. Je suis moi-même tombé dans ce piège dans le passé.

La propagation d'états entre les contrôleurs de vue est essentielle dans toute application iOS. Lorsque vos utilisateurs naviguent sur les écrans de votre application et interagissaient avec celle-ci, vous devez conserver un état global qui enregistre toutes les modifications apportées par l'utilisateur aux données..

Et c’est là que la plupart des développeurs iOS recherchent la solution évidente, mais incorrecte: le modèle singleton.

Le modèle singleton est très rapide à mettre en œuvre, en particulier dans Swift, et cela fonctionne bien. Vous devez simplement ajouter une variable statique à une classe pour conserver une instance partagée de la classe elle-même, et vous avez terminé..

classe Singleton static let shared = Singleton ()

Il est alors facile d'accéder à cette instance partagée à partir de n'importe où dans votre code:

laisser singleton = Singleton.shared

Pour cette raison, de nombreux développeurs pensent avoir trouvé la meilleure solution au problème de la propagation d'états. Mais ils ont tort.

Le motif singleton est en fait considéré comme un anti-motif. Il y a eu de nombreuses discussions à ce sujet dans la communauté du développement. Par exemple, voir cette question relative au dépassement de capacité..

En un mot, les singletons créent les problèmes suivants:

  • Ils introduisent de nombreuses dépendances dans vos classes, ce qui rend plus difficile leur modification ultérieure..
  • Ils rendent l'état global accessible à n'importe quelle partie de votre code. Cela peut créer des interactions complexes difficiles à suivre et causer de nombreux bogues inattendus..
  • Ils rendent vos cours très difficiles à tester, car vous ne pouvez pas les séparer d'un singleton facilement.

À ce stade, certains développeurs pensent: «Ah, j'ai une meilleure solution. Je vais utiliser le AppDéléguer au lieu".

Le problème est que le AppDéléguer classe dans les applications iOS est accessible via le UIApplication instance partagée:

laisser appDelegate = UIApplication.shared.delegate

Mais l'instance partagée de UIApplication est lui-même un singleton. Donc tu n'as rien résolu!

La solution à ce problème est l'injection de dépendance. L'injection de dépendance signifie qu'une classe ne récupère ni ne crée ses propres dépendances, mais les reçoit de l'extérieur..

Pour voir comment utiliser l'injection de dépendance dans les applications iOS et comment activer le partage d'état, nous devons d'abord revenir sur l'un des modèles d'architecture fondamentaux des applications iOS: le modèle Model-View-Controller..

Extension du modèle MVC

En résumé, le modèle MVC indique qu'il existe trois couches dans l'architecture d'une application iOS:

  • La couche de modèle représente les données d'une application.
  • La couche de vue affiche des informations à l'écran et permet une interaction.
  • La couche de contrôleur sert de colle entre les deux autres couches, en déplaçant les données entre elles.

La représentation habituelle du modèle MVC ressemble à ceci:

Le problème est que ce diagramme est faux.

Ce «secret» se cache dans quelques lignes de la documentation d'Apple:

«Il est possible de fusionner les rôles MVC joués par un objet. Ainsi, un objet remplit les rôles de contrôleur et de vue. Dans ce cas, il serait appelé contrôleur de vue. De la même manière, vous pouvez également avoir des objets modèle-contrôleur. ”

De nombreux développeurs pensent que les contrôleurs de vue sont les seuls contrôleurs existant dans une application iOS. Pour cette raison, beaucoup de code finit par être écrit à l'intérieur, faute d'un meilleur emplacement. C'est ce qui amène les développeurs à utiliser des singletons lorsqu'ils ont besoin de propager un état: cela semble être la seule solution possible.

D'après les lignes citées ci-dessus, il est clair que nous pouvons ajouter une nouvelle entité à notre compréhension du modèle MVC: le contrôleur de modèle. Les contrôleurs de modèle traitent le modèle de l'application en remplissant les rôles que le modèle lui-même ne devrait pas remplir. Voici à quoi devrait ressembler le schéma ci-dessus:

L'exemple parfait d'un contrôleur de modèle est utile pour conserver l'état de l'application. Le modèle ne doit représenter que les données de votre application. L'état de l'application ne devrait pas être sa préoccupation.

Cette conservation d'état se termine généralement à l'intérieur des contrôleurs de vue, mais nous avons maintenant un nouvel endroit, le meilleur pour le dire: un contrôleur de modèle. Ce modèle de contrôleur peut ensuite être visualisé pour visualiser les contrôleurs au fur et à mesure qu’ils apparaissent à l’écran par injection de dépendance..

Nous avons résolu l'anti-pattern singleton. Voyons notre solution en pratique avec un exemple.

Propagation d'état entre contrôleurs de vue à l'aide de l'injection de dépendance

Nous allons écrire une application simple pour voir un exemple concret de la façon dont cela fonctionne. L'application va afficher votre devis préféré sur un écran et vous permettre de l'éditer sur un deuxième écran..

Cela signifie que notre application aura besoin de deux contrôleurs de vue, qui devront partager l'état. Après avoir vu le fonctionnement de cette solution, vous pouvez étendre le concept aux applications de toute taille et de toute complexité..

Pour commencer, nous avons besoin d’un type de modèle pour représenter les données, qui dans notre cas est un devis. Cela peut être fait avec une simple structure:

struct Quote let text: String laissez author: String

Le contrôleur de modèle

Nous devons ensuite créer un contrôleur de modèle contenant l'état de l'application. Ce contrôleur de modèle doit être une classe. En effet, nous aurons besoin d’une seule instance que nous transmettrons à tous nos contrôleurs de vue. Les types de valeur tels que les structures sont copiés lorsque nous les transmettons. Ils ne constituent donc manifestement pas la bonne solution..

Tous les besoins de notre contrôleur de modèle dans notre exemple sont une propriété dans laquelle il peut conserver le devis actuel. Mais, bien sûr, dans les plus grandes applications, les contrôleurs de modèles peuvent être plus complexes que cela:

class ModelController var quote = Citation (texte: "Deux choses sont infinies: l'univers et la stupidité humaine; et je ne suis pas sûr de l'univers.", auteur: "Albert Einstein")

J'ai assigné une valeur par défaut à la citation Nous aurons donc déjà quelque chose à afficher à l'écran lors du lancement de l'application. Cela n’est pas nécessaire et vous pouvez déclarer la propriété comme étant une propriété initialisée facultative. néant, si vous souhaitez que votre application soit lancée avec un état vide.

Créer l'interface utilisateur

Nous avons maintenant le contrôleur de modèle, qui contiendra l'état de notre application. Ensuite, nous avons besoin des contrôleurs de vue qui représenteront les écrans de notre application..

Tout d'abord, nous créons leurs interfaces utilisateur. Voici à quoi ressemblent les deux contrôleurs de vue à l'intérieur du storyboard de l'application.

L'interface du premier contrôleur de vue est composée de deux étiquettes et d'un bouton, assemblés avec de simples contraintes de mise en page automatique. (Vous pouvez en savoir plus sur la mise en page automatique ici sur Envato Tuts +.)

L'interface du second contrôleur de vue est la même, mais elle dispose d'une vue texte pour modifier le texte du devis et d'un champ de texte pour modifier l'auteur..

Les deux contrôleurs de vue sont connectés par une seule séquence de présentation modale, qui provient de la Modifier la citation bouton.

Vous pouvez explorer l'interface et les contraintes des contrôleurs de vue dans le référentiel GitHub..

Coder un contrôleur de vue avec injection de dépendance

Nous devons maintenant coder nos contrôleurs de vue. Il est important de garder à l’esprit que nous devons recevoir l’instance de contrôleur de modèle de l’extérieur, par injection de dépendance. Ils doivent donc exposer une propriété à cette fin..

var modelController: ModelController!

Nous pouvons appeler notre premier contrôleur de vue QuoteViewController. Ce contrôleur de vue a besoin de quelques sorties pour les étiquettes du devis et de l'auteur dans son interface..

class QuoteViewController: UIViewController @IBOutlet faible var quoteTextLabel: UILabel! @IBOutlet faible var quoteAuthorLabel: UILabel! var modelController: ModelController! 

Lorsque ce contrôleur de vue s’affiche à l’écran, nous remplissons son interface pour afficher le devis actuel. Nous mettons le code pour le faire dans le contrôleur viewWillAppear (_ :) méthode.

class QuoteViewController: UIViewController @IBOutlet faible var quoteTextLabel: UILabel! @IBOutlet faible var quoteAuthorLabel: UILabel! var modelController: ModelController! remplacer func viewWillAppear (_ animée: Bool) super.viewWillAppear (animée) let quote = modelController.quote quoteTextLabel.text = quote.text quoteAuthorLabel.text = quote.author

Nous aurions pu mettre ce code à l'intérieur du viewDidLoad () méthode à la place, ce qui est assez commun. Le problème, cependant, est que viewDidLoad () est appelé une seule fois, lorsque le contrôleur de vue est créé. Dans notre application, nous devons mettre à jour l'interface utilisateur de QuoteViewController chaque fois qu'il apparaît à l'écran. En effet, l'utilisateur peut modifier la citation sur le deuxième écran.. 

C’est pourquoi nous utilisons le viewWillAppear (_ :) méthode au lieu de viewDidLoad (). De cette manière, nous pouvons mettre à jour l'interface utilisateur du contrôleur de vue à chaque fois qu'elle apparaît à l'écran. Si vous voulez en savoir plus sur le cycle de vie d'un contrôleur de vue et sur toutes les méthodes appelées, j'ai écrit un article détaillant chacune d'entre elles..

Le contrôleur de vue d'édition

Nous devons maintenant coder le deuxième contrôleur de vue. Nous appellerons celui-ci EditViewController.

Classe EditViewController: UIViewController @IBOutlet Variable faible textView: UITextView! @IBOutlet faible var textField: UITextField! var modelController: ModelController! remplacer func viewDidLoad () super.viewDidLoad () let quote = modelController.quote textView.text = quote.text textField.text = quote.author

Ce contrôleur de vue est comme le précédent:

  • Il comporte des points de vente pour la vue texte et le champ de texte que l'utilisateur utilisera pour modifier le devis..
  • Il a une propriété pour l'injection de dépendance de l'instance de contrôleur de modèle.
  • Il remplit son interface utilisateur avant de s'afficher à l'écran.

Dans ce cas, j'ai utilisé le viewDidLoad () méthode parce que ce contrôleur de vue ne s'affiche à l'écran qu'une fois.

Partage de l'Etat

Nous devons maintenant passer l'état entre les deux contrôleurs de vue et le mettre à jour lorsque l'utilisateur modifie le devis..

Nous passons l'état de l'application dans le préparer (pour: l'expéditeur :) méthode de QuoteViewController. Cette méthode est déclenchée par le segment connecté lorsque l'utilisateur appuie sur le bouton Modifier la citation bouton.

class QuoteViewController: UIViewController @IBOutlet faible var quoteTextLabel: UILabel! @IBOutlet faible var quoteAuthorLabel: UILabel! var modelController: ModelController! override func viewWillAppear (_ animated: Bool) super.viewWillAppear (animated) let quote = modelController.quote quoteTextLabel.text = quote.text quoteAutLabel.text = quote.author ignore la fonction (par exemple: UIStoryboardSegue, le même éditeur?): ) si laissez editViewController = segue.destination as? EditViewController editViewController.modelController = modelController

Ici, nous transmettons l'instance de la ModelController qui maintient l'état de l'application. C’est là que l’injection de dépendance pour le EditViewController arrive.

dans le EditViewController, nous devons mettre à jour l'état avec le nouveau devis entré avant de revenir au contrôleur de vue précédent. Nous pouvons le faire dans une action liée à la sauvegarder bouton:

Classe EditViewController: UIViewController @IBOutlet Variable faible textView: UITextView! @IBOutlet faible var textField: UITextField! var modelController: ModelController! override func viewDidLoad () super.viewDidLoad () let quote = modelController.quote textView.text = quote.text textField.text = quote.author @IBAction func save (_ expéditeur: AnyObject) let newQuote = Quote (texte: textView.text, auteur: textField.text!) modelController.quote = newQuote licencier (animation: true, complétion: nil)

Initialiser le contrôleur de modèle

Nous avons presque terminé, mais vous avez peut-être remarqué qu'il nous manque encore quelque chose: le QuoteViewController passe le ModelController au EditViewController par injection de dépendance. Mais qui donne cette instance au QuoteViewController en premier lieu? N'oubliez pas que lors de l'utilisation d'une injection de dépendance, un contrôleur de vue ne doit pas créer ses propres dépendances. Ceux-ci doivent venir de l'extérieur.

Mais il n'y a pas de contrôleur de vue avant la QuoteViewController, parce que c'est le premier contrôleur de vue de notre application. Nous avons besoin d’un autre objet pour créer le ModelController par exemple et de le transmettre à la QuoteViewController.

Cet objet est le AppDéléguer. Le rôle du délégué de l'application consiste à répondre aux méthodes de cycle de vie de l'application et à configurer l'application en conséquence. Une de ces méthodes est application (_: didFinishLaunchingWithOptions :), qui est appelé dès le lancement de l'application. C’est là que nous créons l’instance du ModelController et le passer à la QuoteViewController:

class AppDelegate: UIResponder, UIApplicationDelegate var fenêtre: UIWindow? application func (_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool si let quoteViewController = window? .rootViewController as? QuoteViewController quoteViewController.modelController = ModelController () return true

Notre application est maintenant complète. Chaque contrôleur de vue a accès à l'état global de l'application, mais nous n'utilisons aucun singleton dans notre code..

Vous pouvez télécharger le projet Xcode pour cet exemple d'application dans le tutoriel GitHub repo.

Conclusions

Dans cet article, vous avez vu en quoi l'utilisation de singletons pour propager l'état dans une application iOS est une mauvaise pratique. Les singletons créent beaucoup de problèmes, bien qu’ils soient très faciles à créer et à utiliser.

Nous avons résolu le problème en examinant de plus près le modèle MVC et en comprenant les possibilités qui y sont cachées. Grâce à l'utilisation de contrôleurs de modèles et à l'injection de dépendances, nous avons pu propager l'état de l'application sur tous les contrôleurs de vue sans utiliser de singletons..

Ceci est un exemple d'application simple, mais le concept peut être généralisé aux applications de toute complexité. Il s'agit de la meilleure pratique standard pour propager l'état dans les applications iOS. Je l'utilise maintenant dans toutes les applications que j'écris pour mes clients.

Quelques points à garder à l'esprit lorsque vous développez le concept vers de plus grandes applications:

  • Le contrôleur de modèle peut enregistrer l'état de l'application, par exemple dans un fichier. De cette façon, nos données seront mémorisées chaque fois que nous fermerons l'application. Vous pouvez également utiliser une solution de stockage plus complexe, par exemple Core Data. Ma recommandation est de conserver cette fonctionnalité dans un contrôleur de modèle séparé qui ne s'occupe que du stockage. Ce contrôleur peut ensuite être utilisé par le contrôleur de modèle qui conserve l'état de l'application..
  • Dans une application avec un flux plus complexe, vous aurez plusieurs conteneurs dans votre flux d'application. Ce sont généralement des contrôleurs de navigation, avec parfois un contrôleur à barre de tabulation. Le concept d'injection de dépendance s'applique toujours, mais vous devez prendre en compte les conteneurs. Vous pouvez creuser dans leurs contrôleurs de vue contenus lors de l'injection de dépendance ou créer des sous-classes de conteneur personnalisées qui transmettent le contrôleur de modèle..
  • Si vous ajoutez la mise en réseau à votre application, cela devrait également figurer dans un contrôleur de modèle séparé. Un contrôleur de vue peut effectuer une requête réseau via ce contrôleur de réseau, puis transmettre les données résultantes au contrôleur de modèle qui conserve l'état. Rappelez-vous que le rôle d'un contrôleur de vue est exactement le suivant: agir comme un objet collant qui transfère les données entre les objets..

Restez à l'écoute pour plus de conseils sur le développement d'applications iOS et les meilleures pratiques!