Mettez vos contrôleurs de vision au régime avec MVVM

Dans mon précédent article de cette série, j'avais déjà parlé du modèle Model-View-Controller et de certaines de ses imperfections. Malgré les avantages évidents que MVC apporte au développement de logiciels, il a tendance à être insuffisant dans les applications Cocoa volumineuses ou complexes..

Ce n'est pas une nouvelle, cependant. Plusieurs modèles architecturaux ont émergé au fil des ans, dans le but de remédier aux faiblesses du modèle Model-View-Controller. Vous avez peut-être entendu parler de MVP, Modèle-vue-présentateur, et MVVM, Model-View-ViewModel, par exemple. Ces motifs ressemblent beaucoup au motif Modèle-Vue-Contrôleur, mais ils abordent également certains des problèmes dont souffre le motif Modèle-Vue-Contrôleur..

1. Pourquoi Model-View-ViewModel

J'avais utilisé le modèle Model-View-Controller pendant des années avant de tomber par hasard sur le Model-View-ViewModel modèle. Il n’est pas surprenant que MVVM soit un nouveau venu dans la communauté Cocoa, puisque ses origines remontent à Microsoft. Cependant, le modèle MVVM a été porté sur Cocoa et adapté aux exigences et aux besoins des frameworks Cocoa et a récemment gagné du terrain dans la communauté Cocoa..

Le plus intéressant est le fait que MVVM se présente comme une version améliorée du modèle Model-View-Controller. Cela signifie que cela ne nécessite pas un changement radical de mentalité. En fait, une fois que vous avez compris les principes fondamentaux du modèle, sa mise en œuvre est assez facile, pas plus difficile que de mettre en œuvre le modèle Model-View-Controller..

2. Mettre les contrôleurs de vue sur un régime

Dans le post précédent, j'avais écrit que les contrôleurs d'une application Cocoa typique sont un peu différents des contrôleurs Reenskaug définis dans le modèle MVC d'origine. Sur iOS, par exemple, un contrôleur de vue contrôle une vue. Sa seule responsabilité est de renseigner la vue qu'il gère et de réagir aux interactions de l'utilisateur. Mais ce n’est pas la seule responsabilité des contrôleurs de vue dans la plupart des applications iOS.?

Le modèle MVVM introduit un quatrième composant dans le mix, le voir le modèle, ce qui permet de recentrer le contrôleur de vue. Pour ce faire, il assume certaines des responsabilités du contrôleur de vue. Examinez le diagramme ci-dessous pour mieux comprendre comment le modèle de vue s’intègre dans le modèle Model-View-ViewModel..

Comme l'illustre le diagramme, le contrôleur de vue ne possède plus le modèle. C'est le modèle de vue à qui appartient le modèle, et le contrôleur de vue lui demande les données à afficher..

C'est une différence importante par rapport au modèle Model-View-Controller. Le contrôleur de vue n'a pas d'accès direct au modèle. Le modèle de vue transmet au contrôleur de vue les données qu'il doit afficher dans sa vue..

La relation entre le contrôleur de vue et sa vue reste inchangée. Cela est important car cela signifie que le contrôleur de vue peut se concentrer exclusivement sur le remplissage de sa vue et sur la gestion des interactions de l'utilisateur. C'est ce que le contrôleur de vue a été conçu pour.

Le résultat est assez dramatique. Le contrôleur de vue est soumis à un régime et de nombreuses responsabilités sont déplacées vers le modèle de vue. Vous ne vous retrouvez plus avec un contrôleur de vue couvrant des centaines voire des milliers de lignes de code.

3. Responsabilités du modèle de vue

Vous vous demandez probablement comment le modèle de vue s’intègre dans l’ensemble. Quelles sont les tâches du modèle de vue? Quel est le lien avec le contrôleur de vue? Et qu'en est-il du modèle?

Le diagramme que je vous ai montré plus tôt nous donne quelques indices. Commençons par le modèle. Le modèle n'appartient plus au contrôleur de vue. Le modèle de vue possède le modèle et agit en tant que proxy pour le contrôleur de vue. Chaque fois que le contrôleur de vue a besoin d'une donnée de son modèle de vue, ce dernier demande à son modèle les données brutes et le formate de manière à ce qu'il puisse l'utiliser immédiatement dans sa vue. Le contrôleur de vue n'est pas responsable de la manipulation et du formatage des données.

Le diagramme révèle également que le modèle appartient au modèle de vue, pas au contrôleur de vue. Il convient également de noter que le modèle Model-View-ViewModel respecte la relation étroite qui existe entre le contrôleur de vue et sa vue, caractéristique des applications Cocoa. C'est pourquoi MVVM se sent comme un ajustement naturel pour les applications Cocoa.

4. Un exemple

Le modèle Model-View-ViewModel n'étant pas natif de Cocoa, il n'existe aucune règle stricte pour l'implémenter. Malheureusement, c'est une chose qui déroute beaucoup les développeurs. Pour clarifier quelques points, j'aimerais vous montrer un exemple de base d'une application qui utilise le modèle MVVM. Nous créons une application très simple qui récupère les données météorologiques pour un emplacement prédéfini à partir de l'API Dark Sky et affiche la température actuelle à l'utilisateur..

Étape 1: Configurer le projet

Lancez Xcode et créez un nouveau projet basé sur le Application à vue unique modèle. J'utilise Xcode 8 et Swift 3 pour ce tutoriel.

Nommez le projet MVVM, Et mettre La langue à Rapide et Dispositifs à iPhone.

Étape 2: Créer un modèle de vue

Dans une application Cocoa typique alimentée par le modèle Model-View-Controller, le contrôleur de vue serait en charge de l'exécution de la requête réseau. Vous pouvez utiliser un gestionnaire pour exécuter la requête réseau, mais le contrôleur de vue connaîtra toujours l'origine des données météorologiques. Plus important encore, il recevrait les données brutes et devrait les formater avant de les afficher à l'utilisateur. Ce n'est pas l'approche que nous adoptons lorsque nous adoptons le modèle Model-View-ViewModel.

Créons un modèle de vue. Créez un nouveau fichier Swift, nommez-le WeatherViewViewModel.swift, et définir une classe nommée WeatherViewViewModel.

importer la classe Foundation WeatherViewViewModel 

L'idée est simple. Le contrôleur de vue demande au modèle de vue la température actuelle pour un emplacement prédéfini. Etant donné que le modèle de vue envoie une requête réseau à l'API Dark Sky, la méthode accepte une fermeture, qui est appelée lorsque le modèle de vue contient des données pour le contrôleur de vue. Ces données pourraient être la température actuelle, mais il pourrait également s'agir d'un message d'erreur. C'est ce que le currentTemperature (achèvement :) méthode du modèle de vue ressemble à. Nous allons compléter les détails dans quelques instants.

classe Foundation import WeatherViewViewModel // MARK: - Type d'alias typealias CurrentTemperatureCompletion = (Chaîne) -> Void // MARK: - API publique func currentTemperature (complétion: @écranage de CurrentTemperatureCompletion) 

Nous déclarons un alias de type pour plus de commodité et définissons une méthode, currentTemperature (achèvement :), qui accepte une fermeture de type CurrentTemperatureCompletion

L’implémentation n’est pas difficile si vous connaissez le réseautage et la URLSession API. Regardez le code ci-dessous et remarquez que j'ai utilisé un enum, API, garder tout bien rangé.

classe Foundation import WeatherViewViewModel // MARK: - Type d'alias typealias CurrentTemperatureCompletion = (Chaîne) -> Void // MARK: - API enum API static let lat = 37.8267 statique let long = -122.4233 statique let APIKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx baseURL = URL (chaîne: "https://api.darksky.net/forecast")! static var requestURL: URL return API.baseURL .appendingPathComponent (API.APIKey) .appendingPathComponent ("\ (lat), \ (long)") // MARK: - API publique func currentTemperature (complétion: @escaping CurrentTemperatureCompletion) let dataTask = URLSession.shared.dataTask (avec: API.requestURL) [auto faible] (données, réponse, erreur) dans // Helpers var formatedTemperature: String? if let data = data formateduTempérature = self? .temperature (from: data) DispatchQueue.main.async complétion (formattedTempérature ?? "Impossible d'extraire les données météo") // Reprendre la tâche data dataTask.resume () 

Le seul élément de code que je ne vous ai pas encore montré est la mise en œuvre de la température (de :) méthode. Dans cette méthode, nous extrayons la température actuelle de la réponse Dark Sky.

// MARK: - Méthodes auxiliaires func temperature (à partir de data: Data) -> String? gardien laisse JSON = essayer? JSONSerialization.jsonObject (avec: données, options: []) en tant que? [String: Any] else return nil garde laissez actuellement = JSON? ["Actuellement"] sous la forme? [Chaîne: Any] else return nil garde la température = actuellement ["température"] comme? Double else return nil return String (format: "% .0f ° F", temperature)

Dans une application de production, je choisirais une solution plus robuste pour analyser la réponse, telle que ObjectMapper ou Unbox..

Étape 3: Intégrer le modèle de vue

Nous pouvons maintenant utiliser le modèle de vue dans le contrôleur de vue. Nous créons une propriété pour le modèle de vue et définissons également trois points de vente pour l'interface utilisateur..

importez la classe UIKit ViewController: UIViewController // MARK: - Propriétés @IBOutlet var temperatureLabel: UILabel! // MARK: - @IBOutlet var fetchWeatherDataButton: UIButton! // MARK: - @IBOutlet var activityIndicatorView: UIActivityIndicatorView! // MARK: - private let viewModel = WeatherViewViewModel ()

Notez que le contrôleur de vue est propriétaire du modèle de vue. Dans cet exemple, le contrôleur de vue est également responsable de l'instanciation de son modèle de vue. En général, je préfère injecter le modèle de vue dans le contrôleur de vue, mais restons simple pour l'instant..

Dans le contrôleur de vue viewDidLoad () méthode, nous invoquons une méthode d'assistance, fetchWeatherData ().

// MARK: - Voir le cycle de vie redéfinir func viewDidLoad () super.viewDidLoad () // Extraire les données météo fetchWeatherData ()

Dans fetchWeatherData (), nous demandons au modèle de vue la température actuelle. Avant de demander la température, nous allons masquer l'étiquette et le bouton et afficher la vue de l'indicateur d'activité. Dans la fermeture on passe à fetchWeatherData (achèvement :), nous mettons à jour l'interface utilisateur en remplissant l'étiquette de température et en masquant la vue d'indicateur d'activité.

// MARK: - Méthodes d'assistance private func fetchWeatherData () // Masquer l'interface utilisateur temperatureLabel.isHidden = true fetchWeatherDataButton.isHidden = true // Afficher la vue de l'indicateur d'activité activityIndicatorView.startAnimating () // Extraire la vue des données météoModel.currentTemperature [unowned self] (temperature) in // Mise à jour de l'étiquette de température self.temperatureLabel.text = temperature self.temperatureLabel.isHidden = false // Afficher le bouton d'extraction de données météo self.fetchWeatherDataButton.isHidden = false // Masquer l'indicateur d'activité self.activityIndicatorView.stopAniming ()

Le bouton est relié à une action, fetchWeatherData (_ :), dans lequel on invoque aussi le fetchWeatherData () méthode d'assistance. Comme vous pouvez le constater, la méthode d'assistance nous aide à éviter la duplication de code.

// MARK: - Actions @IBAction func fetchWeatherData (_ sender: Any) // Récupération des données météo fetchWeatherData ()

Étape 4: Créer l'interface utilisateur

La dernière pièce du puzzle consiste à créer l'interface utilisateur de l'application exemple. Ouvrir Tableau principal et ajoutez une étiquette et un bouton à une vue de pile verticale. Nous allons également ajouter une vue d'indicateur d'activité en haut de la vue de la pile, centrée verticalement et horizontalement.

N'oubliez pas de câbler les prises et l'action que nous avons définie dans le ViewController classe!

Maintenant, construisez et exécutez l'application pour l'essayer. N'oubliez pas que vous avez besoin d'une clé API Dark Sky pour que l'application fonctionne. Vous pouvez vous inscrire pour un compte gratuit sur le site Web Dark Sky.

5. Quels sont les avantages?

Même si nous n’avons déplacé que quelques éléments dans le modèle de vue, vous vous demandez peut-être pourquoi cela est nécessaire. Qu'avons-nous gagné? Pourquoi voudriez-vous ajouter cette couche supplémentaire de complexité?

L'avantage le plus évident est que le contrôleur de vue est plus léger et plus concentré sur la gestion de sa vue. C'est la tâche principale d'un contrôleur de vue: gérer sa vue.

Mais il y a un avantage plus subtil. Comme le contrôleur de vue n'est pas responsable de l'extraction des données météorologiques de l'API Dark Sky, il ne connaît pas les détails liés à cette tâche. Les données météorologiques peuvent provenir d'un service météorologique différent ou d'une réponse mise en cache. Le contrôleur de vue ne saurait pas, et il n'a pas besoin de savoir.

Les tests s'améliorent également de façon spectaculaire. Les contrôleurs de vue sont réputés difficiles à tester en raison de leur relation étroite avec la couche de vue. En déplaçant une partie de la logique métier vers le modèle d'affichage, nous améliorons instantanément la testabilité du projet. Tester les modèles de vue est étonnamment facile, car ils n'ont pas de lien vers la couche de vue de l'application.

Conclusion

Le modèle Model-View-ViewModel constitue une avancée significative dans la conception d'applications Cocoa. Les contrôleurs de vue ne sont pas aussi volumineux, les modèles de vues sont plus faciles à composer et à tester, et votre projet devient ainsi plus facile à gérer..

Dans cette courte série, nous n'avons fait qu'effleurer la surface. Il reste encore beaucoup à écrire sur le modèle Model-View-ViewModel. C'est devenu l'un de mes modèles préférés au fil des ans, et c'est pourquoi je continue de parler et d'écrire à ce sujet. Essayez et laissez-moi savoir ce que vous pensez!

En attendant, consultez certains de nos autres articles sur le développement d'applications Swift et iOS..