Premiers pas avec le modèle d’architecture VIPER

Online Coding Courses for Kids

Le modèle architectural VIPER est une alternative à MVC ou MVVM. Et tandis que les frameworks SwiftUI et Combine créent une combinaison puissante qui permet de créer rapidement des interfaces utilisateur complexes et de déplacer des données autour d’une application, ils sont également livrés avec leurs propres défis et opinions sur l’architecture.

C’est une croyance commune que toute la logique de l’application devrait maintenant entrer dans une vue SwiftUI, mais ce n’est pas le cas.

VIPER offre une alternative à ce scénario et peut être utilisé en conjonction avec SwiftUI et Combine pour aider à créer des applications avec une architecture propre qui sépare efficacement les différentes fonctions et responsabilités requises, telles que l’interface utilisateur, la logique métier, le stockage de données et la mise en réseau. Ceux-ci sont ensuite plus faciles à tester, à entretenir et à développer.

Dans ce didacticiel, vous allez créer une application à l’aide du modèle d’architecture VIPER. L’application est également appelée VIPÈRE: Roadtrips faciles à planifier et visuellement intéressants. Intelligent, non? :]

Il permettra aux utilisateurs de créer des trajets routiers en ajoutant des waypoints à un itinéraire. En cours de route, vous découvrirez également SwiftUI et Combine pour vos projets iOS.

Écran principal de l'application VIPER

Commencer

Téléchargez le matériel du projet sur Télécharger les documents en haut ou en bas du didacticiel. Ouvrez le entrée projet. Cela inclut du code pour vous aider à démarrer:

  • le ContentView lancera les autres vues de l’application au fur et à mesure que vous les créez.
  • Il existe des vues d’assistance dans le Vues fonctionnelles groupe: un pour envelopper le MapKit vue de la carte, une vue spéciale “image divisée”, qui est utilisée par le TripListCell. Vous les ajouterez à l’écran dans un petit moment.
  • dans le Entités groupe, vous verrez les classes liées au modèle de données. Voyage et Waypoint servira plus tard en tant qu’entités de l’architecture VIPER. En tant que tels, ils ne contiennent que des données et n’incluent aucune logique fonctionnelle.
  • dans le Source d’information groupe, il existe des fonctions d’assistance pour enregistrer ou charger des données.
  • Jetez un coup d’œil si vous le souhaitez WaypointModule groupe. Cela a une implémentation VIPER de l’écran d’édition Waypoint. Il est inclus avec le démarreur pour que vous puissiez terminer l’application à la fin de ce didacticiel.

Cet exemple utilise Pixabay, un site de partage de photos sous licence autorisée. Pour insérer des images dans l’application, vous devez créer un compte gratuit et obtenir une clé API.

Suivez les instructions ici https://pixabay.com/accounts/register/ pour créer un compte. Ensuite, copiez votre clé API dans le apiKey variable trouvée dans ImageDataProvider.swift. Vous pouvez le trouver dans le Documents sur l’API Pixabay sous Recherche d’images.

Si vous construisez et exécutez maintenant, vous ne verrez rien de trop intéressant.

Application VIPER au projet de démarrage

Cependant, à la fin du didacticiel, vous disposerez d’une application de planification de voyage sur route entièrement fonctionnelle.

Qu’est-ce que VIPER?

VIPER est un modèle architectural comme MVC ou MVVM, mais il sépare davantage le code par une seule responsabilité. Le MVC de style Apple motive les développeurs à mettre toute la logique dans un UIViewController sous-classe. VIPER, comme MVVM avant lui, cherche à résoudre ce problème.

Chacune des lettres VIPÈRE représente un composant de l’architecture: Vue, Interacteur, Présentateur, Entité et Routeur.

  • le Vue est l’interface utilisateur. Cela correspond à un SwiftUI View.
  • le Interacteur est une classe qui sert d’intermédiaire entre le présentateur et les données. Cela prend la direction du présentateur.
  • le Présentateur est le «flic du trafic» de l’architecture, dirigeant les données entre la vue et l’interacteur, prenant les actions de l’utilisateur et appelant au routeur pour déplacer l’utilisateur entre les vues.
  • Une Entité représente les données d’application.
  • le Routeur gère la navigation entre les écrans. C’est différent de ce qu’il est dans SwiftUI, où la vue montre de nouvelles vues.

Cette séparation découle de l’oncle de Bob Martin Paradigme de l’architecture propre.

Diagramme VIPER

Lorsque vous regardez le diagramme, vous pouvez voir qu’il existe un chemin complet pour que les données circulent entre la vue et les entités.

SwiftUI a sa propre façon de faire les choses. Le mappage des responsabilités VIPER sur les objets de domaine sera différent si vous le comparez aux didacticiels pour les applications UIKit.

Comparaison des architectures

Les gens discutent souvent de VIPER avec MVC et MVVM, mais c’est différent de ces modèles.

MVC, ou Model-View-Controller, est le modèle que la plupart des gens associent à l’architecture des applications iOS de 2010. Avec cette approche, vous définissez le Vue dans un storyboard, et le Manette est un associé UIViewController sous-classe. Le contrôleur modifie la vue, accepte les entrées de l’utilisateur et interagit directement avec le Modèle. Le contrôleur regorge de logique d’affichage et de logique métier.

MVVM est une architecture populaire qui sépare la logique de vue de la logique métier dans un Voir le modèle. Le modèle de vue interagit avec le Modèle.

La grande différence est qu’un modèle de vue, contrairement à un contrôleur de vue, n’a qu’une référence unidirectionnelle à la vue et au modèle. MVVM convient parfaitement à SwiftUI, et il existe un didacticiel complet sur le sujet.

VIPÈRE va plus loin en séparant la logique de la vue de la logique du modèle de données. Seul le présentateur parle à la vue, et seul l’interacteur parle au modèle (entité). Le présentateur et l’interacteur se coordonnent. Le présentateur est concerné par l’affichage et l’action de l’utilisateur, et l’interacteur est concerné par la manipulation des données.

Un serpent vipère, pour le plaisir

Définition d’une entité

VIPER est un acronyme amusant pour cette architecture, mais son ordre n’est pas proscrit.

Le moyen le plus rapide d’obtenir quelque chose à l’écran est de commencer par l’entité. L’entité est le ou les objets de données du projet. Dans ce cas, les principales entités sont Voyage, qui contient une liste de Waypoints, qui sont les arrêts du voyage.

L’application contient un Modèle de données classe qui contient une liste de voyages. Le modèle utilise un fichier JSON pour la persistance locale, mais vous pouvez le remplacer par un serveur principal distant sans avoir à modifier le code au niveau de l’interface utilisateur. C’est l’un des avantages d’une architecture propre: lorsque vous modifiez une partie – comme la couche de persistance – elle est isolée des autres zones du code.

Ajout d’un interacteur

Créer un nouveau Fichier Swift nommé TripListInteractor.swift.

Ajoutez le code suivant au fichier:

class TripListInteractor {
  let model: DataModel

  init (model: DataModel) {
    self.model = model
  }
}

Cela crée la classe d’interaction et lui affecte un DataModel, que vous utiliserez plus tard.

Configuration du présentateur

Maintenant, créez un nouveau Fichier Swift nommé TripListPresenter.swift. Ce sera pour la classe des présentateurs. Le présentateur se soucie de fournir des données à l’interface utilisateur et de médier les actions des utilisateurs.

Ajoutez ce code au fichier:

import SwiftUI
import Combine

class TripListPresenter: ObservableObject {
  private let interactor: TripListInteractor

  init(interactor: TripListInteractor) {
    self.interactor = interactor
  }
}

Cela crée une classe de présentateur qui fait référence à l’interacteur.

Comme c’est le travail du présentateur de remplir la vue avec des données, vous voulez exposer la liste des trajets du modèle de données.

Ajoutez une nouvelle variable à la classe:

@Published var trips: [Trip] = []

Il s’agit de la liste des trajets que l’utilisateur verra dans la vue. En le déclarant avec le @Published wrapper de propriété, la vue pourra écouter les modifications apportées à la propriété et se mettre à jour automatiquement.

L’étape suivante consiste à synchroniser cette liste avec le modèle de données de l’interacteur. Tout d’abord, ajoutez la propriété d’assistance suivante:

private var cancellables = Set()

Cet ensemble est un endroit pour stocker les abonnements Combine afin que leur durée de vie soit liée à celle de la classe. De cette façon, tous les abonnements resteront actifs tant que le présentateur sera présent.

Ajoutez le code suivant à la fin de init(interactor:):

interactor.model.$trips
  .assign(to: .trips, on: self)
  .store(in: &cancellables)

interactor.model.$trips crée un éditeur qui suit les modifications apportées au modèle de données trips collection. Ses valeurs sont affectées à la propre classe de cette classe trips , créant un lien qui tient à jour les déplacements du présentateur lorsque le modèle de données change.

Enfin, cet abonnement est stocké dans cancellables afin que vous puissiez le nettoyer plus tard.

Construire une vue

Vous devez maintenant construire le premier Vue: la vue de la liste de voyages.

Création d’une vue avec un présentateur

Créez un nouveau fichier à partir du SwiftUI View modèle et nommez-le TripListView.swift.

Ajoutez la propriété suivante à TripListView:

@ObservedObject var presenter: TripListPresenter

Cela relie le présentateur à la vue. Ensuite, corrigez les aperçus en modifiant le corps de TripListView_Previews.previews à:

let model = DataModel.sample
let interactor = TripListInteractor(model: model)
let presenter = TripListPresenter(interactor: interactor)
return TripListView(presenter: presenter)

Maintenant, remplacez le contenu de TripListView.body avec:

List {
  ForEach (presenter.trips, id: .id) { item in
    TripListCell(trip: item)
      .frame(height: 240)
  }
}

Cela crée un List où les voyages du présentateur sont énumérés, et il génère un pré-fourni TripListCell pour chaque.

Fenêtre d'aperçu de la vue de la liste de voyages

Modification du modèle à partir de la vue

Jusqu’à présent, vous avez vu un flux de données de l’entité vers l’interacteur via le présentateur pour remplir la vue. Le modèle VIPER est encore plus utile lors de l’envoi d’actions utilisateur pour manipuler le modèle de données.

Pour voir cela, vous allez ajouter un bouton pour créer un nouveau voyage.

Tout d’abord, ajoutez ce qui suit à la classe dans TripListInteractor.swift:

func addNewTrip() {
  model.pushNewTrip()
}

Cela enveloppe le modèle pushNewTrip(), ce qui crée un nouveau Trip en haut de la liste des voyages.

Puis dans TripListPresenter.swift, ajoutez ceci à la classe:

func makeAddNewButton() -> some View {
  Button(action: addNewTrip) {
    Image(systemName: "plus")
  }
}

func addNewTrip() {
  interactor.addNewTrip()
}

Cela crée un bouton avec le système + image avec une action qui appelle addNewTrip(). Cela transmet l’action à l’interacteur, qui manipule le modèle de données.

Revenir à TripListView.swift et ajoutez ce qui suit après la List accolade de fermeture:

.navigationBarTitle("Roadtrips", displayMode: .inline)
.navigationBarItems(trailing: presenter.makeAddNewButton())

Cela ajoute le bouton et un titre à la barre de navigation. Modifiez maintenant le return dans TripListView_Previews comme suit:

return NavigationView {
  TripListView(presenter: presenter)
}

Cela vous permet de voir la barre de navigation en mode aperçu.

Reprenez l’aperçu en direct pour voir le bouton.

Liste des trajets avec bouton en aperçu en direct

Le voir en action

C’est le bon moment pour revenir en arrière et câbler TripListView au reste de l’application.

Ouvert ContentView.swift. Dans le corps de view, remplace le VStack avec:

TripListView(presenter:
  TripListPresenter(interactor:
    TripListInteractor(model: model)))

Cela crée la vue avec son présentateur et son interacteur. Maintenant, construisez et exécutez.

Taper sur + bouton ajoutera un Nouveau voyage à la liste.

Liste de voyages avec un nouveau voyage ajouté

Supprimer un voyage

Les utilisateurs qui créent des voyages voudront probablement aussi pouvoir les supprimer en cas d’erreur ou lorsque le voyage est terminé. Maintenant que vous avez créé le chemin de données, l’ajout d’actions supplémentaires à l’écran est simple.

Dans TripListInteractor, ajouter:

func deleteTrip(_ index: IndexSet) {
  model.trips.remove(atOffsets: index)
}

Cela supprime les éléments de la trips collecte dans le modèle de données. Parce que c’est un @Published , l’interface utilisateur sera automatiquement mise à jour en raison de son abonnement aux modifications.

Dans TripListPresenter, ajouter:

func deleteTrip(_ index: IndexSet) {
  interactor.deleteTrip(index)
}

Cela transmet la commande de suppression à l’interacteur.

Enfin, dans TripListView, ajoutez ce qui suit après l’accolade d’extrémité du ForEach:

.onDelete(perform: presenter.deleteTrip)

Ajout d’un .onDelete à un élément dans un SwiftUI List active automatiquement le balayage pour supprimer le comportement. L’action est ensuite envoyée au présentateur, donnant le coup d’envoi à toute la chaîne.

Construisez et exécutez, et vous pourrez désormais supprimer les trajets!

Avec onDelete, l'action Supprimer est activée.

Routage vers la vue détaillée

Il est maintenant temps d’ajouter le Routeur fait partie de VIPER.

Un routeur permettra à l’utilisateur de naviguer de la vue de liste de voyages à la vue de détail de voyage. La vue détaillée du trajet affichera une liste des points de route ainsi qu’une carte de l’itinéraire.

L’utilisateur pourra éditer la liste des waypoints et le nom du trajet depuis cet écran.

Yay Router!

Configuration des écrans de détails du trajet

Avant d’afficher l’écran de détail, vous devez le créer.

En suivant l’exemple précédent, créez deux nouveaux Fichier Swifts: TripDetailPresenter.swift et TripDetailInteractor.swift et un SwiftUI View nommé TripDetailView.swift.

Définissez le contenu de TripDetailInteractor à:

import Combine
import MapKit

class TripDetailInteractor {
  private let trip: Trip
  private let model: DataModel
  let mapInfoProvider: MapDataProvider

  private var cancellables = Set()

  init (trip: Trip, model: DataModel, mapInfoProvider: MapDataProvider) {
    self.trip = trip
    self.mapInfoProvider = mapInfoProvider
    self.model = model
  }
}

Cela crée une nouvelle classe pour l’interacteur de l’écran de détail du voyage. Cela interagit avec deux sources de données: un individu Trip et les informations cartographiques de MapKit. Il existe également un ensemble pour les abonnements annulables que vous ajouterez ultérieurement.

Puis dans TripDetailPresenter, définissez son contenu sur:

import SwiftUI
import Combine

class TripDetailPresenter: ObservableObject {
  private let interactor: TripDetailInteractor

  private var cancellables = Set()

  init(interactor: TripDetailInteractor) {
    self.interactor = interactor
  }
}

Cela crée un stub presenter avec une référence pour l’interacteur et l’ensemble annulable. Vous allez développer cela dans un peu.

Dans TripDetailView, ajoutez la propriété suivante:

@ObservedObject var presenter: TripDetailPresenter

Cela ajoute une référence au présentateur dans la vue.

Pour obtenir à nouveau la construction des aperçus, remplacez ce talon par:

static var previews: some View {
    let model = DataModel.sample
    let trip = model.trips[1]
    let mapProvider = RealMapDataProvider()
    let presenter = TripDetailPresenter(interactor:
      TripDetailInteractor(
        trip: trip,
        model: model,
        mapInfoProvider: mapProvider))
    return NavigationView {
      TripDetailView(presenter: presenter)
    }
  }

La vue va maintenant se construire, mais l’aperçu est toujours juste “Bonjour, monde!”

Juste l'aperçu de la vue par défaut

Acheminement

Avant de créer la vue détaillée, vous devez la lier au reste de l’application via un routeur à partir de la liste des trajets.

Créer un nouveau Fichier Swift nommé TripListRouter.swift.

Définissez son contenu sur:

import SwiftUI

class TripListRouter {
  func makeDetailView(for trip: Trip, model: DataModel) -> some View {
    let presenter = TripDetailPresenter(interactor:
      TripDetailInteractor(
        trip: trip,
        model: model,
        mapInfoProvider: RealMapDataProvider()))
    return TripDetailView(presenter: presenter)
  }
}

Cette classe génère une nouvelle TripDetailView qui a été rempli avec un interacteur et un présentateur. Le routeur gère la transition d’un écran à un autre, configurant les classes nécessaires pour la vue suivante.

Dans un paradigme impératif de l’interface utilisateur – en d’autres termes, avec UIKit – un routeur serait responsable de la présentation des contrôleurs de vue ou de l’activation des séquences.

SwiftUI déclare toutes les vues cibles comme faisant partie de la vue actuelle et les affiche en fonction de l’état de la vue. Pour mapper VIPER sur SwiftUI, la vue est désormais responsable de l’affichage / masquage des vues, le routeur est un générateur de vue de destination et le présentateur coordonne les deux.

Dans TripListPresenter.swift, ajoutez le routeur en tant que propriété:

private let router = TripListRouter()

Vous avez maintenant créé le routeur dans le cadre du présentateur.

Ensuite, ajoutez cette méthode:

func linkBuilder(
    for trip: Trip,
    @ViewBuilder content: () -> Content
  ) -> some View {
    NavigationLink(
      destination: router.makeDetailView(
        for: trip,
        model: interactor.model)) {
          content()
    }
}

Cela crée un NavigationLink à une vue détaillée fournie par le routeur. Lorsque vous le placez dans un NavigationView, le lien devient un bouton qui pousse le destination sur la pile de navigation.

le content bloc peut être n’importe quelle vue SwiftUI arbitraire. Mais dans ce cas, le TripListView fournira un TripListCell.

Aller à TripListView.swift et changer le contenu du ForEach à:

self.presenter.linkBuilder(for: item) {
  TripListCell(trip: item)
    .frame(height: 240)
}

Cela utilise le NavigationLink du présentateur, définit la cellule comme son contenu et la place dans la liste.

Construisez et exécutez, et maintenant, lorsque l’utilisateur appuie sur la cellule, il les achemine vers un «Hello World» TripDetailView.

Écran de détail Hello World

Fin de la vue de détail

Il reste quelques détails de voyage dont vous avez besoin pour remplir la vue détaillée afin que l’utilisateur puisse voir l’itinéraire et modifier les waypoints.

Commencez par ajouter un titre de voyage:

Dans TripDetailInteractor, ajoutez les propriétés suivantes:

var tripName: String { trip.name }
var tripNamePublisher: Published.Publisher { trip.$name }

Cela expose juste le String version du nom du voyage et un Publisher pour quand ce nom change.

Ajoutez également les éléments suivants:

func setTripName(_ name: String) {
  trip.name = name
}

func save() {
  model.save()
}

La première méthode permet au présentateur de modifier le nom du trajet et la seconde enregistre le modèle dans la couche de persistance.

Maintenant, passez à TripDetailPresenter. Ajoutez les propriétés suivantes:

@Published var tripName: String = "No name"
let setTripName: Binding

Ceux-ci fournissent les crochets pour que la vue puisse lire et définir le nom du trajet.

Ensuite, ajoutez ce qui suit à la init méthode:

// 1
setTripName = Binding(
  get: { interactor.tripName },
  set: { interactor.setTripName($0) }
)

// 2
interactor.tripNamePublisher
  .assign(to: .tripName, on: self)
  .store(in: &cancellables)

Ce code:

  1. Crée une liaison pour définir le nom du trajet. le TextField l’utilisera dans la vue pour pouvoir lire et écrire à partir de la valeur.
  2. Attribue le nom du voyage de l’éditeur de l’interacteur à la tripName propriété du présentateur. Cela maintient la valeur synchronisée.

La séparation du nom du trajet en propriétés comme celle-ci vous permet de synchroniser la valeur sans créer une boucle infinie de mises à jour.

Ensuite, ajoutez ceci:

func save() {
  interactor.save()
}

Cela ajoute une fonction d’enregistrement afin que l’utilisateur puisse enregistrer tous les détails modifiés.

Enfin, allez à TripDetailViewet remplacez le body avec:

var body: some View {
  VStack {
    TextField("Trip Name", text: presenter.setTripName)
      .textFieldStyle(RoundedBorderTextFieldStyle())
      .padding([.horizontal])
  }
  .navigationBarTitle(Text(presenter.tripName), displayMode: .inline)
  .navigationBarItems(trailing: Button("Save", action: presenter.save))
}

le VStack détient pour l’instant un TextField pour modifier le nom du trajet. Les modificateurs de la barre de navigation définissent le titre à l’aide de la publication du présentateur tripName, il est donc mis à jour au fur et à mesure que l’utilisateur tape et un bouton d’enregistrement qui conserve les modifications

Construisez et exécutez, et maintenant, vous pouvez modifier le titre du voyage.

Modifier le nom dans la vue détaillée

Enregistrez après avoir modifié le nom du voyage et les modifications apparaîtront après avoir relancé l’application.

Les modifications persistent après l'enregistrement

Utilisation d’un deuxième présentateur pour la carte

L’ajout de widgets supplémentaires à un écran suivra le même schéma de:

  • Ajout de fonctionnalités à l’interacteur.
  • Faire le pont entre la fonctionnalité et le présentateur.
  • Ajout des widgets à la vue.

Aller à TripDetailInteractoret ajoutez les propriétés suivantes:

@Published var totalDistance: Measurement =
  Measurement(value: 0, unit: .meters)
@Published var waypoints: [Waypoint] = []
@Published var directions: [MKRoute] = []

Ceux-ci fournissent les informations suivantes sur les waypoints d’un voyage: la distance totale en Measurement, la liste des points de cheminement et une liste des directions qui relient ces points de cheminement.

Ensuite, ajoutez les abonnements suivants à la fin de init(trip:model:mapInfoProvider:):

trip.$waypoints
  .assign(to: .waypoints, on: self)
  .store(in: &cancellables)

trip.$waypoints
  .flatMap { mapInfoProvider.totalDistance(for: $0) }
  .map { Measurement(value: $0, unit: UnitLength.meters) }
  .assign(to: .totalDistance, on: self)
  .store(in: &cancellables)

trip.$waypoints
  .setFailureType(to: Error.self)
  .flatMap { mapInfoProvider.directions(for: $0) }
  .catch { _ in Empty<[MKRoute], Never>() }
  .assign(to: .directions, on: self)
  .store(in: &cancellables)

Cela effectue trois actions distinctes basées sur la modification des points de cheminement du voyage.

Le premier n’est qu’une copie de la liste des points de cheminement de l’interacteur. Le second utilise le mapInfoProvider pour calculer la distance totale pour tous les points de cheminement. Et le troisième utilise le même fournisseur de données pour obtenir les directions entre les waypoints.

Le présentateur utilise ensuite ces valeurs pour fournir des informations à l’utilisateur.

Aller à TripDetailPresenteret ajoutez ces propriétés:

@Published var distanceLabel: String = "Calculating..."
@Published var waypoints: [Waypoint] = []

La vue utilisera ces propriétés. Câblez-les pour suivre les modifications des données en ajoutant ce qui suit à la fin de init(interactor:):

interactor.$totalDistance
  .map { "Total Distance: " + MeasurementFormatter().string(from: $0) }
  .replaceNil(with: "Calculating...")
  .assign(to: .distanceLabel, on: self)
  .store(in: &cancellables)

interactor.$waypoints
  .assign(to: .waypoints, on: self)
  .store(in: &cancellables)

Le premier abonnement prend la distance brute de l’interacteur et le formate pour l’afficher dans la vue, et le second copie simplement sur les waypoints.

Compte tenu de la vue Carte

Avant de vous diriger vers la vue détaillée, considérez la vue de la carte. Ce widget est plus compliqué que les autres.

En plus de dessiner les caractéristiques géographiques, l’application superpose également des broches pour chaque point et l’itinéraire entre eux.

Cela nécessite son propre ensemble de logique de présentation. Vous pouvez utiliser le TripDetailPresenter, ou dans ce cas, créez un TripMapViewPresenter. Il réutilisera le TripDetailInteractor car il partage le même modèle de données et est une vue en lecture seule.

Créer un nouveau Fichier Swift nommé TripMapViewPresenter.swift. Définissez son contenu sur:

import MapKit
import Combine

class TripMapViewPresenter: ObservableObject {
  @Published var pins: [MKAnnotation] = []
  @Published var routes: [MKRoute] = []

  let interactor: TripDetailInteractor
  private var cancellables = Set()

  init(interactor: TripDetailInteractor) {
    self.interactor = interactor

    interactor.$waypoints
      .map {
        $0.map {
          let annotation = MKPointAnnotation()
          annotation.coordinate = $0.location
          return annotation
        }
    }
    .assign(to: .pins, on: self)
    .store(in: &cancellables)

    interactor.$directions
      .assign(to: .routes, on: self)
      .store(in: &cancellables)
  }
}

Ici, le présentateur de carte expose deux tableaux pour contenir des annotations et des itinéraires. Dans init(interactor:), vous mappez le waypoints de l’interacteur à MKPointAnnotation objets afin qu’ils puissent être affichés sous forme d’épingles sur la carte. Vous copiez ensuite le directions à la routes tableau.

Pour utiliser le présentateur, créez un nouveau SwiftUI View nommé TripMapView.swift. Définissez son contenu sur:

import SwiftUI

struct TripMapView: View {
  @ObservedObject var presenter: TripMapViewPresenter

  var body: some View {
    MapView(pins: presenter.pins, routes: presenter.routes)
  }
}

#if DEBUG
struct TripMapView_Previews: PreviewProvider {
  static var previews: some View {
    let model = DataModel.sample
    let trip = model.trips[0]
    let interactor = TripDetailInteractor(
      trip: trip,
      model: model,
      mapInfoProvider: RealMapDataProvider())
    let presenter = TripMapViewPresenter(interactor: interactor)
    return VStack {
      TripMapView(presenter: presenter)
    }
  }
}
#endif

Cela utilise l’aide MapView et lui fournit des broches et des itinéraires du présentateur. le previews struct construit la chaîne VIPER dont l’application a besoin pour prévisualiser uniquement la carte. Utilisation Aperçu en direct pour voir la carte correctement:

Fenêtre d'aperçu avec TripMapView

Pour ajouter la carte à l’application, ajoutez d’abord la méthode suivante à TripDetailPresenter:

func makeMapView() -> some View {
   TripMapView(presenter: TripMapViewPresenter(interactor: interactor))
}

Cela fait une vue de carte, en lui fournissant son présentateur.

Ensuite, ouvrez TripDetailView.swift.

Ajoutez ce qui suit à la VStack sous le TextField:

presenter.makeMapView()
Text(presenter.distanceLabel)

Construisez et exécutez pour voir la carte à l’écran:

Affichage de la carte fonctionnant dans l'application

Modification des waypoints

La dernière fonctionnalité consiste à ajouter la modification des waypoints afin que vous puissiez faire vos propres voyages! Vous pouvez réorganiser la liste dans la vue détaillée du trajet. Mais pour créer un nouveau waypoint, vous aurez besoin d’une nouvelle vue pour que l’utilisateur puisse taper le nom.

Pour accéder à une nouvelle vue, vous aurez besoin d’un Routeur. Créer un nouveau Fichier Swift nommé TripDetailRouter.swift.

Ajoutez ce code au nouveau fichier:

import SwiftUI

class TripDetailRouter {
  private let mapProvider: MapDataProvider

  init(mapProvider: MapDataProvider) {
    self.mapProvider = mapProvider
  }

  func makeWaypointView(for waypoint: Waypoint) -> some View {
    let presenter = WaypointViewPresenter(
      waypoint: waypoint,
      interactor: WaypointViewInteractor(
        waypoint: waypoint,
        mapInfoProvider: mapProvider))
    return WaypointView(presenter: presenter)
  }
}

Cela crée un WaypointView qui est déjà configuré et prêt à fonctionner.

Avec le routeur à portée de main, accédez à TripDetailInteractor.swiftet ajoutez les méthodes suivantes:

func addWaypoint() {
   trip.addWaypoint()
}

func moveWaypoint(fromOffsets: IndexSet, toOffset: Int) {
 trip.waypoints.move(fromOffsets: fromOffsets, toOffset: toOffset)
}

func deleteWaypoint(atOffsets: IndexSet) {
  trip.waypoints.remove(atOffsets: atOffsets)
}

func updateWaypoints() {
  trip.waypoints = trip.waypoints
}

Ces méthodes sont auto-descriptives. Ils ajoutent, déplacent, suppriment et mettent à jour des waypoints.

Ensuite, exposez-les à la vue à travers TripDetailPresenter. Dans TripDetailPresenter, ajoutez cette propriété:

private let router: TripDetailRouter

Cela tiendra le routeur. Créez-le en ajoutant ceci en haut de init(interactor:):

self.router = TripDetailRouter(mapProvider: interactor.mapInfoProvider)

Cela crée le routeur à utiliser avec l’éditeur de waypoints. Ensuite, ajoutez ces méthodes:

func addWaypoint() {
  interactor.addWaypoint()
}

func didMoveWaypoint(fromOffsets: IndexSet, toOffset: Int) {
  interactor.moveWaypoint(fromOffsets: fromOffsets, toOffset: toOffset)
}

func didDeleteWaypoint(_ atOffsets: IndexSet) {
  interactor.deleteWaypoint(atOffsets: atOffsets)
}

func cell(for waypoint: Waypoint) -> some View {
  let destination = router.makeWaypointView(for: waypoint)
    .onDisappear(perform: interactor.updateWaypoints)
  return NavigationLink(destination: destination) {
    Text(waypoint.name)
  }
}

Les trois premiers font partie des opérations sur le waypoint. La dernière méthode appelle le routeur pour obtenir une vue de waypoint pour le waypoint et le mettre dans un NavigationLink.

Enfin, montrez-le à l’utilisateur dans TripDetailView en ajoutant ce qui suit à la VStack sous le Text:

HStack {
  Spacer()
  EditButton()
  Button(action: presenter.addWaypoint) {
    Text("Add")
  }
}.padding([.horizontal])
List {
  ForEach(presenter.waypoints, content: presenter.cell)
    .onMove(perform: presenter.didMoveWaypoint(fromOffsets:toOffset:))
    .onDelete(perform: presenter.didDeleteWaypoint(_:))
}

Cela ajoute les contrôles suivants à la vue:

  • Une EditButton qui met la liste en mode édition afin que l’utilisateur puisse déplacer ou supprimer des waypoints.
  • Un ajout Button qui utilise le présentateur pour ajouter un nouveau waypoint à la liste.
  • UNE List qui utilise un ForEach avec le présentateur pour créer une cellule pour chaque waypoint. La liste définit un onMove et onDelete action qui permet à ces actions de modification et rappelle dans le présentateur.

Construisez et exécutez, et vous pouvez maintenant personnaliser un voyage! N’oubliez pas d’enregistrer les modifications.

Waypoints ajoutés à l'écran de détail
L'éditeur Waypoint

Faire des modules

Avec VIPER, vous pouvez regrouper le présentateur, l’interacteur, la vue, le routeur et le code associé en modules.

Traditionnellement, un module exposait les interfaces du présentateur, de l’interacteur et du routeur dans un seul contrat. Cela n’a pas beaucoup de sens avec SwiftUI car c’est une vue vers l’avant. À moins que vous ne vouliez empaqueter chaque module comme son propre framework, vous pouvez plutôt conceptualiser les modules en tant que groupes.

Prendre TripListView.swift, TripListPresenter.swift, TripListInteractor.swift et TripListRouter.swift et les regrouper dans un groupe nommé TripListModule.

Faites de même pour les classes de détail: TripDetailView.swift, TripDetailPresenter.swift, TripDetailInteractor.swift, TripMapViewPresenter.swift, TripMapView.swift, et TripDetailRouter.swift.

Ajoutez-les à un nouveau groupe appelé TripDetailModule.

Les modules sont un bon moyen de garder le code propre et séparé. En règle générale, un module doit être un écran / une fonctionnalité conceptuelle, et les routeurs transfèrent l’utilisateur entre les modules.

Où aller en partant d’ici?

Clique le Télécharger les documents en haut ou en bas du didacticiel pour télécharger les fichiers de projet terminés.

L’un des avantages de la séparation approuvée par VIPER est la testabilité. Vous pouvez tester l’interacteur afin qu’il puisse lire et manipuler le modèle de données. Et vous pouvez faire tout cela en testant indépendamment le présentateur pour changer la vue et répondre aux actions de l’utilisateur.

Considérez-le comme un exercice amusant à essayer par vous-même!

En raison de la puissance réactive de Combine et de son support natif dans SwiftUI, vous avez peut-être remarqué que les couches d’interaction et de présentation sont relativement minces. Ils séparent les préoccupations, mais la plupart du temps, ils ne font que transmettre des données à travers une couche d’abstraction.

Avec SwiftUI, il est un peu plus naturel de réduire les fonctionnalités du présentateur et de l’interaction en un seul ObservableObject qui détient la majeure partie de l’état d’affichage et interagit directement avec les entités.

Pour une approche alternative, lisez MVVM avec Combine Tutorial pour iOS.

Nous espérons que vous avez apprécié ce tutoriel! Si vous pensez à des questions ou des commentaires, déposez-les dans la discussion ci-dessous. Nous aimerions connaître votre architecture préférée et ce qui a changé à l’ère de SwiftUI.

Close Menu