Combiner les MVVM basés dans les applications UIKit et SwiftUI pour les développeurs UIKit



Nous savons que les ObservableObjecclasses t avec ses @Publishedpropriétés sont créées Combinespécialement pour View Modeldans SwiftUI. Mais exactement la même chose View Modelpeut être utilisée dans la UIKitmise en œuvre de l'architecture  MVVM, bien que dans ce cas, nous devrons «lier» ( bind) manuellement les UIéléments aux @Published propriétés de View Model. Vous serez surpris, mais avec l'aide de Combinecela, vous pouvez faire quelques lignes de code. De plus, en adhérant à cette idéologie lors de la conception d' UIKitapplications, vous passerez ensuite sans douleur à SwiftUI.

Le but de cet article est de montrer avec un exemple simple primitivement comment vous pouvez implémenter élégamment une MVVMarchitecture UIKitavec  Combine. Par contraste, nous montrons l'utilisation de la mêmeView Model c SwiftUI.

L'article abordera deux applications simples qui vous permettent de sélectionner les dernières informations météorologiques pour une ville spécifique à partir du site OpenWeatherMa p. Mais l' UIun d'eux sera créé avec application SwiftUI, et l'autre avec aide UIKit. Pour l'utilisateur, ces applications seront presque identiques.



Le code est sur Github .

L'interface utilisateur ( UI) ne contiendra que 2 UI éléments: un champ de texte pour entrer la ville et une étiquette pour afficher la température. La zone de texte pour entrer dans la ville est l'entrée active ( Input) et l'étiquette de température est la sortie passive ( Output).  

Le rôle View Model dans l'architecture MVVMest qu'il prend les ENTRÉES de View(ou ViewControllerdans UIKit), implémente la logique métier de l'application et retransmet les SORTIES à  View(ou ViewControllerdans UIKit), présentant éventuellement ces données dans le format souhaité.

Créer  View Modelavec Combinen'importe quel type de logique métier - synchrone ou asynchrone - est très simple si vous utilisez une ObservableObject classe avec ses @Publishedpropriétés.

API OpenWeatherMap


Bien que le service  OpenWeatherMap vous   permette de sélectionner des informations météorologiques très complètes, le modèle des données qui nous intéressent sera très simple, il fournit des informations détaillées  WeatherDetailsur la météo actuelle dans la ville sélectionnée et se trouve dans le fichier  Model.swift : bien



que dans cette tâche spécifique, nous ne nous intéresserons qu'à la température temp, qui est dans la structure  Main, le modèle fournit des informations détaillées complètes sur la météo actuelle en tant que structure racine  WeatherDetail, croyant qu'à l'avenir vous voudrez étendre les capacités de cette application. La structure WeatherDetail est codable, cela nous permettra de décoder littéralement les JSONdonnées dans le modèle avec seulement deux lignes de code  .

La structure  WeatherDetail devrait également êtreIdentifiablesi nous voulons nous faciliter à l'avenir l'affichage d'un tableau de prévisions météorologiques  [WeatherDetail] pour plusieurs jours à l'avance sous la forme d'une liste  List de SwiftUI. C'est également un blanc pour une future application météo actuelle plus sophistiquée. Le protocole Identifiablenécessite la présence de la propriété id,que nous avons déjà, donc aucun effort supplémentaire ne sera requis de notre part.

Habituellement, les services, y compris le service  OpenWeatherMap , offrent toutes sortes de services URLs pour obtenir les ressources dont nous avons besoin. Le service  OpenWeatherMap nous offre URLsde récupérer des informations détaillées sur la météo actuelle ou les prévisions pour 5 jours dans une certaine ville city. Dans cette application, nous ne serons intéressés que par les informations météo actuelles et pour ce casURLcalculé à l'aide de la fonction absoluteURL (city: String):



API pour le service OpenWeatherMap ,  nous le placerons dans le fichier WeatherAPI.swift . Sa partie centrale sera une méthode de sélection d'informations météorologiques détaillées  WeatherDetaildans une ville  city:

  • fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>

Dans le contexte du cadre, Combine cette méthode renvoie non seulement des informations météorologiques détaillées  WeatherDetail, mais l '"éditeur" correspondant Publisher. Notre «éditeur» AnyPublisher<WeatherDetail, Never>ne renvoie aucune erreur - Neveret si une erreur d'échantillonnage ou de codage s'est toujours produite, le sous-ministre revient  WeatherDetail.placeholdersans aucun message supplémentaire sur la cause de l'erreur. 

Considérez plus en détail la méthode  fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>qui sélectionne  les informations météorologiques détaillées pour la ville sur le site Web OpenWeatherMap city et ne renvoie aucune erreur Never:



  1. en fonction du nom de la ville, nous  city formons en URL utilisant la fonction absoluteURL(city:city)pour demander des informations météorologiques détaillées  WeatherDetail,
  2. «» dataTaskPublisher(for:), Output (data: Data, response: URLResponse),   Failure - URLError,
  3. map { } (data: Data, response: URLResponse)  data
  4. JSON  data ,  WeatherDetail, ,
  5. - «»  catch (error ... )  «» WeatherDetail.placeholder,
  6. main , UI,
  7. «» «» eraseToAnyPublisher() AnyPublisher.

L '«éditeur» asynchrone ainsi obtenu AnyPublisher«ne décolle pas» de lui-même, il ne livre rien tant que quelqu'un n'y «souscrit» pas. Nous allons l'utiliser dans une  ObservableObject classe qui joue un rôle View Modelà la SwiftUIfois dans  et UIKit

Créer un modèle de vue


Pour View Modelcréer une classe très simple  TempViewModelqui implémente un protocole ObservableObject avec deux  @Published propriétés:  



  1. l'un  @Published var city: Stringest la ville (vous pouvez l'appeler conditionnellement une ENTRÉE, car sa valeur est réglée par l'utilisateur sur View),  
  2. la seconde  @Published var currentWeather = WeatherDetail.placeholder est la météo dans cette ville en ce moment (nous pouvons conditionnellement appeler cette propriété EXIT, car elle est obtenue en récupérant les données du site Web  OpenWeatherMap ).

Une fois que nous avons défini une  @Published propriété  city, nous pouvons commencer à l'utiliser à la fois comme simple propriété  cityet comme "éditeur"  $city.

Dans une classe  TempViewModel, vous pouvez non seulement déclarer les propriétés qui nous intéressent, mais également prescrire la logique métier de leur interaction. À cette fin, lors de l'initialisation d'une instance de la classe  TempViewModel dans, init?nous pouvons créer un «abonnement» qui fonctionnera tout au long du «cycle de vie» de l'instance de la classe  TempViewModelet reproduira la dépendance de la météo actuelle  currentWeather sur la ville  city.

Pour ce faire, Combinenous étirons la chaîne de l'entrée "éditeur" $city à la sortie "éditeur" AnyPublisher<WeatherDetail, Never>, dont la valeur est la météo actuelle. Par la suite, nous y «souscrivons» avec l'aide d'un «abonné» assign (to: \.currentWeather, on: self) et obtenir la valeur souhaitée de la météo actuelle en  currentWeather tant que @Published propriété de «sortie»  .

Nous devons tirer la chaîne PAS simplement des propriétés city, à savoir des "éditeurs" $cityqui participeront à la création UI et c'est là que nous la changerons.

Comment allons-nous procéder?

Nous avons déjà une fonction dans notre arsenal fetchWeather (for city: String)qui est dans la classe  WeatherAPIet renvoie «l'éditeur» AnyPublisher<WeatherDetail, Never> avec des informations météorologiques détaillées en fonction de la ville  city, et nous ne pouvons qu'en quelque sorte utiliser la valeur de «l'éditeur»  $citypour en faire un argument de cette fonction.

 Allez chez le bon éditeur  fetchWeather (for city: String) pour  Combinenous aider opérateur  flatMap:



OpérateurflatMapcrée un nouvel «éditeur» sur la base des données reçues de l'ancien «éditeur».

Ensuite, nous «souscrivons» à cet «éditeur» nouvellement reçu avec l'aide d'un «abonné» très simple  assign (to: \.currentWeather, on: self)et attribuons la valeur reçue de «l'éditeur» à la @Publishedpropriété  currentWeather:



nous venons de créer un init( )«éditeur» ASYNCHRONE et « nous y sommes abonnés», ce qui donne lieu à un AnyCancellable«abonnement» ".

AnyCancellable L '«abonnement» permet à l'appelant d'annuler à tout moment «l'abonnement» et de ne plus recevoir de valeurs de «l'éditeur», mais de plus, dès que  AnyCancellable«l'abonnement» quitte son champ d'application, la mémoire occupée par «l'éditeur» est libérée. Par conséquent, dès qu'il sera init( ) terminé, cet «abonnement» sera supprimé par le système ARC, et ne pas avoir le temps d'attribuer les informations asynchrones sur la météo actuelle reçues avec un retard  currentWeather. Pour enregistrer un tel «abonnement», il est nécessaire de créer une init()variable EXTÉRIEURE var cancellableSetqui conservera notre AnyCancellable«abonnement» dans cette variable tout au long du «cycle de vie» de l'instance de classe  TempViewMode

L ' AnyCancellable«abonnement» dans la variable est stocké cancellableSetà l'aide de l'opérateur  store ( in: &self.cancellableSet):



Par conséquent, l' «abonnement» sera conservé tout au long du «cycle de vie» de l'instance de classe  TempViewModel. Nous pouvons modifier la valeur de l'éditeur comme vous le souhaitez $city, et la météo actuelle currentWeather pour cette ville sera toujours à notre disposition .

Afin de réduire le nombre d'appels de serveur lors de la saisie d'une ville city, nous ne devons pas utiliser directement «l'éditeur» de la ligne avec le nom de la ville  $city, mais sa version modifiée avec les opérateurs debounceet removeDuplicates:



L'opérateur est  debounce utilisé pour attendre jusqu'à ce que l'utilisateur ait fini de taper les informations nécessaires sur le clavier, puis effectuer une seule fois la tâche gourmande en ressources.

De même, un opérateur removeDuplicatesne publiera des valeurs que si elles diffèrent des valeurs précédentes. Par exemple, si l'utilisateur entre d'abord john, puis joe, puis à nouveau john, nous ne recevrons johnqu'une seule fois. Cela contribue à rendre le nôtre UIplus efficace.

Création d'une interface utilisateur avec SwiftUI


Maintenant que nous l'avons View Model, commençons UI. D'abord à SwiftUI, puis à UIKit.

Dans nous Xcodecréons un nouveau projet avec SwiftUIet dans la structure résultante, nous ContentView  plaçons View Modelle nôtre  comme @ObservedObject variable model. Remplacez  Text ("Hello, World!") par le titre  Text ("WeatherApp"), ajoutez une zone de texte pour entrer la ville  TextField ("City", text: self.$model.city) et une étiquette pour afficher la température:



Nous avons directement utilisé les valeurs de notre variable model: TempViewModel(). Nous avons utilisé dans la zone de texte pour entrer la ville $model.city, et dans l'étiquette pour afficher la température - model.currentWeather.main?.temp.

Désormais, toute modification des  @Published propriétés entraînera un "redessin" View:



ceci est assuré par le fait que le nôtre View Model est@ObservedObject, c'est-à-dire que la «liaison» AUTOMATIQUE ( binding) @Publishedde nos propriétés View Modelet des éléments de l'interface utilisateur ( UI) est effectuée . Cette "liaison" AUTOMATIQUE n'est possible qu'en SwiftUI.

Création d'une interface utilisateur avec UIKit


Que faire avec ça UIKit? Ce n’est pas là  @ObservedObject. Dans  UIKit nous effectuerons la "liaison" ( binding) manuellement. Il existe plusieurs façons de procéder à cette «liaison manuelle»:

  • Key-Value Observing ou KVO: un mécanisme permettant  key pathsde surveiller une propriété et de recevoir une notification de sa modification.
  • Programmation réactive fonctionnelle ou FRP: utilisation d'un framework Combine.
  • Delegation: Utilisation de méthodes déléguées pour envoyer une notification indiquant qu'une valeur de propriété a changé.
  • Boxing: didSet { } , .

Compte tenu du titre de l'article, nous travaillerons naturellement sur le terrain Combine. Dans l' UIKitapplication, nous montrerons à quel point il est facile de créer une «liaison manuelle» avec Combine.

Dans l' UIKitapplication, nous aurons également deux UI éléments: UITextFieldpour entrer dans la ville et UILabelpour afficher la température. Dans ViewControllernous aurons naturellement Outletces éléments:





Sous la forme d'une variable ordinaire viewModel, nous avons le même que View Modeldans la section précédente:



Avant de faire une «liaison manuelle» avec Combine, faisons du champ de texte UITextFieldnotre allié et «éditeur» de notre contenu text:



Cela nous permettra de mettre en viewDidLoadœuvre très facilement la «liaison manuelle» en utilisant la fonctionbinding (): En



effet, nous « subscribe » à « éditeur » cityTextField.textPublisheravec l'aide d'un « abonné » très simple  assign (to: \.city, on: viewModel)et assignons le texte tapé par l'utilisateur dans le champ de texte cityTextFieldà notre « entrée »  @Publishedpropriété de la citynôtre View Model.

De plus, nous apportons des changements dans une autre direction: nous «souscrivons» à la @Publishedpropriété  «sortie»  $currentWeather avec l'aide de «l'abonné» sink et sa fermeture receiveValue, formons la valeur de température et l'affectons à l'étiquette temperatureLabel.

Reçu dans  viewDidLoad «l'abonnement» est stocké dans une variable var cancellableSet. Après les avoir créés une fois, nous leur permettons d'agir tout au long du «cycle de vie» de l'instance de classe ViewControlleret avec «l'abonnement» dans notre View Modelimplémenter toute la logique métier de l'application.

Soit dit en passant, le protocole ObservableObjectne fonctionne pas avec UIKit, mais il n'interfère pas. UIKit complètement indifférent au protocole ObservableObjectet, en principe, il pourrait être supprimé  View Modeldans les UIKit applications:



mais nous ne le ferons pas, car nous voulons le garder inchangé View Modelà la fois pour l'application actuelle UIKitet éventuellement pour les applications futures SwiftUI.

C'est tout. Le code est sur Github .

Conclusion

Un cadre réactif fonctionnel Combinevous permet de mettre en œuvre l' MVVMarchitecture de manière très simple et concise à la fois SwiftUIsous UIKitforme de code compréhensible et lisible.

Liens:

Combiner + UIKit + MVVM
Utilisation du
didacticiel Combiner MVVM iOS: Refactorisation de MVC
MVVM avec le didacticiel Combiner pour iOS

PS Si vous voulez voir des informations météorologiques, vous devez vous inscrire sur OpenWeatherMap  et obtenir API key. Ce processus ne vous prendra pas plus de 2 minutes.

All Articles