Erste Schritte mit dem VIPER-Architekturmuster

Das VIPER-Architekturmuster ist eine Alternative zu MVC oder MVVM. Und während die Frameworks SwiftUI und Combine eine leistungsstarke Kombination bilden, mit der komplexe Benutzeroberflächen schnell erstellt und Daten in einer App verschoben werden können, haben sie auch ihre eigenen Herausforderungen und Meinungen zur Architektur.

Es ist allgemein bekannt, dass die gesamte App-Logik jetzt in eine SwiftUI-Ansicht übernommen werden sollte, dies ist jedoch nicht der Fall.

VIPER bietet eine Alternative zu diesem Szenario und kann in Verbindung mit SwiftUI und Combine verwendet werden, um Apps mit einer sauberen Architektur zu erstellen, die die verschiedenen erforderlichen Funktionen und Verantwortlichkeiten wie Benutzeroberfläche, Geschäftslogik, Datenspeicherung und Netzwerk effektiv voneinander trennt. Diese sind dann leichter zu testen, zu warten und zu erweitern.

In diesem Tutorial erstellen Sie eine App mit dem VIPER-Architekturmuster. Die App heißt auch bequem VIPER: Visuell interessante geplante einfache Roadtrips. Clever, oder? :]

Damit können Benutzer Roadtrips erstellen, indem sie einer Route Wegpunkte hinzufügen. Auf dem Weg dorthin lernen Sie auch SwiftUI und Combine für Ihre iOS-Projekte kennen.

Hauptbildschirm der VIPER-App

Erste Schritte

Laden Sie die Projektmaterialien über die Schaltfläche Materialien herunterladen oben oder unten im Lernprogramm herunter. Öffnen Sie das Starter-Projekt. Dies enthält Code zum Einstieg:

  • Die ContentView startet die anderen Ansichten der App, während Sie sie erstellen.
  • Es gibt einige Hilfsansichten in der Gruppe Funktionale Ansichten: eine zum Umschließen der MapKit-Kartenansicht, eine spezielle „Split Image“ -Ansicht, die von TripListCell verwendet wird. Sie werden diese in kürze zum Bildschirm hinzufügen.
  • In der Gruppe Entitäten sehen Sie die Klassen, die sich auf das Datenmodell beziehen. Trip und Waypoint werden später als Entitäten der VIPER-Architektur dienen. Als solche enthalten sie nur Daten und enthalten keine funktionale Logik.
  • In der Gruppe Datenquellen gibt es die Hilfsfunktionen zum Speichern oder Laden von Daten.
  • Peek voraus, wenn Sie in der WaypointModule Gruppe mögen. Dies hat eine VIPER-Implementierung des Wegpunktbearbeitungsbildschirms. Es ist im Starter enthalten, sodass Sie die App bis zum Ende dieses Tutorials abschließen können.

In diesem Beispiel wird eine permissiv lizenzierte Foto-Sharing-Site verwendet. Um Bilder in die App zu ziehen, müssen Sie ein kostenloses Konto erstellen und einen API-Schlüssel erhalten.

Folgen Sie den Anweisungen hier https://.com/accounts/register/ um ein Konto zu erstellen. Kopieren Sie dann Ihren API-Schlüssel in die Variable apiKey in ImageDataProvider .rasch. Sie finden es in den API-Dokumenten unter Bilder suchen.

Wenn Sie jetzt bauen und ausführen, werden Sie nichts zu Interessantes sehen.

VIPER-App im Starter-Projekt

Am Ende des Tutorials haben Sie jedoch eine voll funktionsfähige Roadtrip-Planungs-App.

Was ist VIPER?

VIPER ist ein Architekturmuster wie MVC oder MVVM, aber es trennt den Code weiter durch einzelne Verantwortlichkeiten. MVC im Apple-Stil motiviert Entwickler, die gesamte Logik in eine UIViewController -Unterklasse einzufügen. VIPER versucht, wie zuvor MVVM, dieses Problem zu beheben.

Jeder der Buchstaben in VIPER steht für eine Komponente der Architektur: View, Interactor, Presenter, Entity und Router.

  • Die Ansicht ist die Benutzeroberfläche. Dies entspricht einem SwiftUI View .
  • Der Interactor ist eine Klasse, die zwischen dem Presenter und den Daten vermittelt. Es nimmt die Richtung vom Moderator.
  • Der Moderator ist der „Verkehrspolizist“ der Architektur, der Daten zwischen der Ansicht und dem Interactor leitet, Benutzeraktionen ausführt und den Router aufruft, um den Benutzer zwischen Ansichten zu verschieben.
  • Eine Entität repräsentiert Anwendungsdaten.
  • Der Router übernimmt die Navigation zwischen den Bildschirmen. Das ist anders als in SwiftUI, wo die Ansicht alle neuen Ansichten anzeigt.

Diese Trennung wird von „Onkel“ Bob Martins sauberem Architekturparadigma getragen.

VIPER-Diagramm

Wenn Sie sich das Diagramm ansehen, können Sie sehen, dass es einen vollständigen Pfad für den Datenfluss zwischen der Ansicht und den Entitäten gibt.

SwiftUI hat seine eigene Art, Dinge zu tun. Die Zuordnung von VIPER-Verantwortlichkeiten zu Domänenobjekten unterscheidet sich, wenn Sie dies mit Tutorials für UIKit-Apps vergleichen.

Architekturen vergleichen

Die Leute diskutieren VIPER oft mit MVC und MVVM, aber es unterscheidet sich von diesen Mustern.

MVC oder Model-View-Controller ist das Muster, das die meisten Menschen mit der iOS-App-Architektur von 2010 in Verbindung bringen. Bei diesem Ansatz definieren Sie die Ansicht in einem Storyboard, und der Controller ist eine zugeordnete UIViewController -Unterklasse. Der Controller ändert die Ansicht, akzeptiert Benutzereingaben und interagiert direkt mit dem Modell. Der Controller wird mit Ansichtslogik und Geschäftslogik aufgebläht.

MVVM ist eine beliebte Architektur, die die Ansichtslogik von der Geschäftslogik in einem Ansichtsmodell trennt. Das Ansichtsmodell interagiert mit dem Modell.

Der große Unterschied besteht darin, dass ein Ansichtsmodell im Gegensatz zu einem Ansichtscontroller nur einen einseitigen Verweis auf die Ansicht und das Modell hat. MVVM passt gut zu SwiftUI und es gibt ein ganzes Tutorial zu diesem Thema.

VIPER geht noch einen Schritt weiter, indem es die Ansichtslogik von der Datenmodelllogik trennt. Nur der Präsentator spricht mit der Ansicht und nur der Interactor spricht mit dem Modell (der Entität). Der Moderator und der Interactor koordinieren sich. Der Präsentator befasst sich mit Anzeige und Benutzeraktion, und der Interactor befasst sich mit der Manipulation der Daten.

Eine Viperschlange, zum Spaß

Definieren einer Entität

VIPER ist ein lustiges Akronym für diese Architektur, aber ihre Reihenfolge ist nicht verboten.

Der schnellste Weg, etwas auf den Bildschirm zu bringen, ist, mit der Entität zu beginnen. Die Entität ist das/die Datenobjekt(e) für das Projekt. In diesem Fall sind die Hauptentitäten Reise, die eine Liste von Wegpunkten enthält, die die Haltestellen in der Reise sind.

Die App enthält eine DataModel-Klasse, die eine Liste der Fahrten enthält. Das Modell verwendet eine JSON-Datei für die lokale Persistenz, aber Sie können diese durch ein Remote-Back-End ersetzen, ohne den Code auf UI-Ebene ändern zu müssen. Das ist einer der Vorteile einer sauberen Architektur: Wenn Sie einen Teil — wie die Persistenzschicht — ändern, ist er von anderen Bereichen des Codes isoliert.

Hinzufügen eines Interaktors

Erstellen Sie eine neue Swift-Datei mit dem Namen TripListInteractor.rasch.

Fügen Sie der Datei den folgenden Code hinzu:

class TripListInteractor { let model: DataModel init (model: DataModel) { self.model = model }}

Dies erstellt die Interactor-Klasse und weist ihr eine DataModel zu, die Sie später verwenden werden.

Einrichten des Präsentators

Erstellen Sie nun eine neue Swift-Datei mit dem Namen TripListPresenter.rasch. Dies wird für die Presenter-Klasse sein. Der Moderator kümmert sich um die Bereitstellung von Daten für die Benutzeroberfläche und die Vermittlung von Benutzeraktionen.

Fügen Sie der Datei diesen Code hinzu:

import SwiftUIimport Combineclass TripListPresenter: ObservableObject { private let interactor: TripListInteractor init(interactor: TripListInteractor) { self.interactor = interactor }}

Dadurch wird eine Presenter-Klasse erstellt, die auf den Interactor verweist.

Da es die Aufgabe des Präsentators ist, die Ansicht mit Daten zu füllen, möchten Sie die Liste der Fahrten aus dem Datenmodell verfügbar machen.

Fügen Sie der Klasse eine neue Variable hinzu:

@Published var trips: = 

Dies ist die Liste der Fahrten, die der Benutzer in der Ansicht sehen wird. Durch Deklarieren mit dem @Published Property Wrapper kann die Ansicht Änderungen an der Eigenschaft abhören und sich automatisch aktualisieren.

Der nächste Schritt besteht darin, diese Liste mit dem Datenmodell aus dem Interactor zu synchronisieren. Fügen Sie zunächst die folgende Hilfseigenschaft hinzu:

private var cancellables = Set<AnyCancellable>()

Dieses Set ist ein Ort, um ihre Abonnements zu speichern, so dass ihre Lebensdauer an die der Klasse gebunden ist. Auf diese Weise bleiben alle Abonnements aktiv, solange der Moderator in der Nähe ist.

Fügen Sie den folgenden Code am Ende von init(interactor:) hinzu:

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

interactor.model.$trips erstellt einen Publisher, der Änderungen an der trips -Sammlung des Datenmodells verfolgt. Seine Werte werden der eigenen trips -Sammlung dieser Klasse zugewiesen, wodurch ein Link erstellt wird, der die Daten des Präsentators aktualisiert, wenn sich das Datenmodell ändert.

Schließlich wird dieses Abonnement in cancellablesgespeichert, damit Sie es später bereinigen können.

Erstellen einer Ansicht

Sie müssen nun die erste Ansicht erstellen: die Trip-Listenansicht.

Erstellen einer Ansicht mit einem Presenter

Erstellen Sie eine neue Datei aus der SwiftUI-Ansichtsvorlage und nennen Sie sie TripListView.rasch.

Fügen Sie die folgende Eigenschaft zu TripListView:

@ObservedObject var presenter: TripListPresenter

Dies verknüpft den Presenter mit der Ansicht. Als nächstes korrigieren Sie die Vorschauen, indem Sie den Text von TripListView_Previews.previews ändern in:

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

Ersetzen Sie nun den Inhalt von TripListView.body durch:

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

Dadurch wird eine List wo die Reisen des Moderators aufgezählt werden, und es wird jeweils ein vordefiniertes TripListCell generiert.

Vorschaufenster der Trip-Listenansicht

Ändern des Modells aus der Ansicht

Bisher haben Sie den Datenfluss von der Entität zum Interactor durch den Presenter gesehen, um die Ansicht zu füllen. Das VIPER-Muster ist noch nützlicher, wenn Benutzeraktionen zurückgesendet werden, um das Datenmodell zu manipulieren.

Um dies zu sehen, fügen Sie eine Schaltfläche hinzu, um eine neue Reise zu erstellen.

Fügen Sie zunächst der Klasse in TripListInteractor Folgendes hinzu.rasch:

func addNewTrip() { model.pushNewTrip()}

Dies umschließt die pushNewTrip() des Modells, wodurch eine neue Trip oben in der Modellliste erstellt wird.

Dann in TripListPresenter.fügen Sie nun Folgendes zur Klasse hinzu:

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

Dies erstellt eine Schaltfläche mit dem System + Bild mit einer Aktion, die addNewTrip() aufruft. Dadurch wird die Aktion an den Interaktor weitergeleitet, der das Datenmodell manipuliert.

Gehen Sie zurück zu TripListView.swift und fügen Sie nach der List schließenden Klammer Folgendes hinzu:

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

Dies fügt der Navigationsleiste die Schaltfläche und einen Titel hinzu. Ändern Sie nun die return in TripListView_Previews wie folgt:

return NavigationView { TripListView(presenter: presenter)}

Dadurch können Sie die Navigationsleiste im Vorschaumodus sehen.

Setzen Sie die Live-Vorschau fort, um die Schaltfläche zu sehen.

Trip-Liste mit Schaltfläche in Live-Vorschau

In Aktion sehen

Jetzt ist ein guter Zeitpunkt, um zurückzukehren und TripListView mit dem Rest der Anwendung zu verbinden.

Öffnen Sie contentView.rasch. Ersetzen Sie im Hauptteil von view die VStack durch:

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

Dies erstellt die Ansicht zusammen mit ihrem Präsentator und Interactor. Jetzt bauen und laufen.

Durch Tippen auf die Schaltfläche + wird eine neue Reise zur Liste hinzugefügt.

Reiseliste mit einer neuen Reise hinzugefügt

Löschen einer Reise

Benutzer, die Reisen erstellen, möchten diese wahrscheinlich auch löschen können, falls sie einen Fehler machen oder wenn die Reise vorbei ist. Nachdem Sie den Datenpfad erstellt haben, ist das Hinzufügen zusätzlicher Aktionen zum Bildschirm einfach.

Fügen Sie in TripListInteractor Folgendes hinzu:

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

Dadurch werden Elemente aus der trips -Sammlung im Datenmodell entfernt. Da es sich um eine @Published -Eigenschaft handelt, wird die Benutzeroberfläche aufgrund ihres Abonnements für die Änderungen automatisch aktualisiert.

Fügen Sie in TripListPresenter Folgendes hinzu:

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

Dies leitet den Löschbefehl an den Interactor weiter.

Fügen Sie schließlich in TripListView nach der Endklammer des ForEach:

.onDelete(perform: presenter.deleteTrip)

Hinzufügen eines .onDeletezu einem Element in einem SwiftUI List aktiviert automatisch das Swipe-to-Delete-Verhalten. Die Aktion wird dann an den Moderator gesendet und startet die gesamte Kette.

Erstellen und ausführen, und Sie werden nun in der Lage sein, Ausflüge zu entfernen!

Mit onDelete ist die Löschaktion aktiviert.

Routing zur Detailansicht

Jetzt ist es an der Zeit, den Router-Teil von VIPER hinzuzufügen.

Ein Router ermöglicht es dem Benutzer, von der Trip-Listenansicht zur Trip-Detailansicht zu navigieren. Die Tourendetailansicht zeigt eine Liste der Wegpunkte zusammen mit einer Karte der Route.

Der Benutzer kann die Liste der Wegpunkte und den Namen der Reise von diesem Bildschirm aus bearbeiten.

Yay Router!

Einrichten der Detailbildschirme

Bevor der Detailbildschirm angezeigt wird, müssen Sie ihn erstellen.

Erstellen Sie nach dem vorherigen Beispiel zwei neue Swift-Dateien: TripDetailPresenter.swift und TripDetailInteractor.swift und eine SwiftUI-Ansicht namens TripDetailView.rasch.

Setzen Sie den Inhalt von TripDetailInteractor auf:

import Combineimport MapKitclass TripDetailInteractor { private let trip: Trip private let model: DataModel let mapInfoProvider: MapDataProvider private var cancellables = Set<AnyCancellable>() init (trip: Trip, model: DataModel, mapInfoProvider: MapDataProvider) { self.trip = trip self.mapInfoProvider = mapInfoProvider self.model = model }}

Hiermit wird eine neue Klasse für den Interactor des Tripdetailbildschirms erstellt. Dies interagiert mit zwei Datenquellen: einer einzelnen Trip und Karteninformationen aus MapKit. Es gibt auch einen Satz für die kündbaren Abonnements, die Sie später hinzufügen werden.

Setzen Sie dann in TripDetailPresenter den Inhalt auf:

import SwiftUIimport Combineclass TripDetailPresenter: ObservableObject { private let interactor: TripDetailInteractor private var cancellables = Set<AnyCancellable>() init(interactor: TripDetailInteractor) { self.interactor = interactor }}

Dies erstellt einen Stub Presenter mit einer Referenz für interactor und cancellable set. Du wirst das gleich aufbauen.

Fügen Sie in TripDetailView die folgende Eigenschaft hinzu:

@ObservedObject var presenter: TripDetailPresenter

Dies fügt einen Verweis auf den Präsentator in der Ansicht hinzu.

Um die Vorschau wieder zu erstellen, ändern Sie diesen Stub in:

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

Jetzt wird die Ansicht erstellt, aber die Vorschau ist immer noch nur „Hallo, Welt!“

Nur die Vorschau der Standardansicht

Routing

Bevor Sie die Detailansicht erstellen, sollten Sie sie über einen Router aus der Trip-Liste mit dem Rest der App verknüpfen.

Erstellen Sie eine neue Swift-Datei mit dem Namen TripListRouter.rasch.

Setzen Sie den Inhalt auf:

import SwiftUIclass 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) }}

Diese Klasse gibt ein neues TripDetailView aus, das mit einem Interactor und Presenter gefüllt wurde. Der Router übernimmt den Übergang von einem Bildschirm zum anderen und richtet die für die nächste Ansicht erforderlichen Klassen ein.

In einem imperativen UI—Paradigma — mit anderen Worten, mit UIKit – wäre ein Router für die Darstellung von View-Controllern oder die Aktivierung von Segmenten verantwortlich.

SwiftUI deklariert alle Zielansichten als Teil der aktuellen Ansicht und zeigt sie basierend auf dem Ansichtsstatus an. Um VIPER auf SwiftUI abzubilden, ist die Ansicht jetzt für das Ein- / Ausblenden von Ansichten verantwortlich, der Router ist ein Zielansichts-Builder und der Präsentator koordiniert zwischen ihnen.

In TripListPresenter.fügen Sie dann den Router als Eigenschaft hinzu:

private let router = TripListRouter()

Sie haben den Router nun als Teil des Presenters erstellt.

Fügen Sie als nächstes diese Methode hinzu:

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

Dadurch wird eine NavigationLink zu einer Detailansicht erstellt, die der Router bereitstellt. Wenn Sie es in einem NavigationView platzieren, wird der Link zu einer Schaltfläche, die die destination auf den Navigationsstapel drückt.

Der content-Block kann eine beliebige SwiftUI-Ansicht sein. Aber in diesem Fall liefert die TripListView eine TripListCell .

Gehe zu TripListView.swift und ändern Sie den Inhalt der ForEach zu:

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

Dies verwendet die NavigationLink aus dem Presenter, setzt die Zelle als Inhalt und fügt sie in die Liste ein.

Erstellen und ausführen, und jetzt, wenn der Benutzer auf die Zelle tippt, werden sie zu einer „Hallo Welt“ weitergeleitet TripDetailView.

Detailbildschirm Hallo Welt

Beenden der Detailansicht

Es gibt noch einige Reisedetails, die Sie in der Detailansicht ausfüllen müssen, damit der Benutzer die Route sehen und die Wegpunkte bearbeiten kann.

Fügen Sie zunächst den Titel der Reise hinzu:

Fügen Sie in TripDetailInteractor die folgenden Eigenschaften hinzu:

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

Dies macht nur die String Version des Reisenamens und eine Publisher wenn sich dieser Name ändert.

Fügen Sie außerdem Folgendes hinzu:

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

Die erste Methode ermöglicht es dem Moderator, den Namen der Reise zu ändern, und die zweite speichert das Modell auf der Persistenzebene.

Gehen Sie nun zu TripDetailPresenter. Fügen Sie die folgenden Eigenschaften hinzu:

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

Diese stellen die Hooks für die Ansicht zum Lesen und Festlegen des Tripnamens bereit.

Fügen Sie dann der init -Methode Folgendes hinzu:

// 1setTripName = Binding<String>( get: { interactor.tripName }, set: { interactor.setTripName($0) })// 2interactor.tripNamePublisher .assign(to: \.tripName, on: self) .store(in: &cancellables)

Dieser Code:

  1. Erstellt eine Bindung zum Festlegen des Reisenamens. Die TextField verwendet dies in der Ansicht, um den Wert lesen und schreiben zu können.
  2. Weist der Eigenschaft tripName des Presenters den Tripnamen aus dem Publisher des Interactors zu. Dadurch bleibt der Wert synchronisiert.

Wenn Sie den Trip-Namen in solche Eigenschaften aufteilen, können Sie den Wert synchronisieren, ohne eine Endlosschleife von Aktualisierungen zu erstellen.

Als nächstes fügen Sie Folgendes hinzu:

func save() { interactor.save()}

Dies fügt eine Speicherfunktion hinzu, damit der Benutzer alle bearbeiteten Details speichern kann.

Schließlich gehen Sie zu TripDetailView, und ersetzen Sie die body mit:

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

Die VStack denn jetzt hält eine TextField zum Bearbeiten des Reisenamens. Die Navigationsleistenmodifikatoren definieren den Titel mit dem Namen tripName des Vortragenden, so dass er mit den Benutzertypen aktualisiert wird, und einer Schaltfläche zum Speichern, die alle Änderungen beibehält.

Erstellen und ausführen, und jetzt können Sie den Titel der Reise bearbeiten.

Bearbeiten Sie den Namen in der Detailansicht

Speichern Sie nach dem Bearbeiten des Reisenamens, und die Änderungen werden nach dem Neustart der App angezeigt.

Änderungen bleiben nach dem Speichern bestehen

Verwenden eines zweiten Präsentators für die Karte

Das Hinzufügen zusätzlicher Widgets zu einem Bildschirm folgt dem gleichen Muster wie:

  • Hinzufügen von Funktionen zum Interactor.
  • Überbrückung der Funktionalität durch den Presenter.
  • Hinzufügen der Widgets zur Ansicht.

Gehen Sie zu TripDetailInteractor und fügen Sie die folgenden Eigenschaften hinzu:

@Published var totalDistance: Measurement<UnitLength> = Measurement(value: 0, unit: .meters)@Published var waypoints: = @Published var directions: = 

Diese liefern die folgenden Informationen über die Wegpunkte in einer Reise: die Gesamtstrecke als Measurement, die Liste der Wegpunkte und eine Liste der Richtungen, die diese Wegpunkte verbinden.

Fügen Sie dann den folgenden Befehl am Ende von init(trip:model:mapInfoProvider:) hinzu:

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<, Never>() } .assign(to: \.directions, on: self) .store(in: &cancellables)

Dies führt drei separate Aktionen aus, die auf der Änderung der Wegpunkte der Reise basieren.

Die erste ist nur eine Kopie der Wegpunktliste des Interactors. Die zweite verwendet die mapInfoProvider , um die Gesamtentfernung für alle Wegpunkte zu berechnen. Und der dritte verwendet denselben Datenanbieter, um Wegbeschreibungen zwischen den Wegpunkten abzurufen.

Der Moderator verwendet diese Werte dann, um dem Benutzer Informationen zur Verfügung zu stellen.

Gehen Sie zu TripDetailPresenter und fügen Sie diese Eigenschaften hinzu:

@Published var distanceLabel: String = "Calculating..."@Published var waypoints: = 

Die Ansicht verwendet diese Eigenschaften. Verdrahten Sie sie, um Datenänderungen zu verfolgen, indem Sie am Ende von init(interactor:) Folgendes hinzufügen:

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)

Das erste Abonnement nimmt die rohe Entfernung vom Interactor und formatiert sie für die Anzeige in der Ansicht, und das zweite kopiert nur die Wegpunkte.

Betrachten der Kartenansicht

Bevor Sie zur Detailansicht übergehen, betrachten Sie die Kartenansicht. Dieses Widget ist komplizierter als die anderen.

Zusätzlich zum Zeichnen der geografischen Merkmale überlagert die App auch Pins für jeden Punkt und die Route zwischen ihnen.

Dies erfordert eine eigene Präsentationslogik. Sie können die TripDetailPresenter oder in diesem Fall eine separate TripMapViewPresenter . Es wird die TripDetailInteractor wiederverwendet, da es dasselbe Datenmodell verwendet und eine schreibgeschützte Ansicht ist.

Erstellen Sie eine neue Swift-Datei mit dem Namen TripMapViewPresenter.rasch. Setzen Sie den Inhalt auf:

import MapKitimport Combineclass TripMapViewPresenter: ObservableObject { @Published var pins: = @Published var routes: = let interactor: TripDetailInteractor private var cancellables = Set<AnyCancellable>() 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) }}

Hier macht der Kartenpräsenter zwei Arrays für Anmerkungen und Routen verfügbar. In init(interactor:) ordnen Sie die waypoints aus dem Interaktor MKPointAnnotation Objekte zu, damit sie als Pins auf der Karte angezeigt werden können. Sie kopieren dann das directions in das routes Array.

Um den Presenter zu verwenden, erstellen Sie eine neue SwiftUI-Ansicht mit dem Namen TripMapView.rasch. Setzen Sie den Inhalt auf:

import SwiftUIstruct TripMapView: View { @ObservedObject var presenter: TripMapViewPresenter var body: some View { MapView(pins: presenter.pins, routes: presenter.routes) }}#if DEBUGstruct TripMapView_Previews: PreviewProvider { static var previews: some View { let model = DataModel.sample let trip = model.trips let interactor = TripDetailInteractor( trip: trip, model: model, mapInfoProvider: RealMapDataProvider()) let presenter = TripMapViewPresenter(interactor: interactor) return VStack { TripMapView(presenter: presenter) } }}#endif

Dies verwendet den Helfer MapView und versorgt ihn mit Pins und Routen vom Presenter. Die previews -Struktur erstellt die VIPER-Kette, die die App benötigt, um nur eine Vorschau der Karte anzuzeigen. Verwenden Sie die Live-Vorschau, um die Karte richtig anzuzeigen:

Vorschaufenster mit TripMapView

Um die Karte zur App hinzuzufügen, fügen Sie zuerst die folgende Methode zu TripDetailPresenter hinzu:

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

Dadurch wird eine Kartenansicht erstellt und mit ihrem Presenter versehen.

Öffnen Sie als nächstes TripDetailView.rasch.

Fügen Sie dem VStack unter dem TextFieldFolgendes hinzu:

presenter.makeMapView()Text(presenter.distanceLabel)

Erstellen und ausführen, um die Karte auf dem Bildschirm anzuzeigen:

Kartenansicht funktioniert in der App

Bearbeiten von Wegpunkten

Das letzte Feature ist das Hinzufügen von Wegpunktbearbeitung, damit Sie Ihre eigenen Reisen machen können! Sie können die Liste in der Reisedetailansicht neu anordnen. Um jedoch einen neuen Wegpunkt zu erstellen, benötigen Sie eine neue Ansicht, in die der Benutzer den Namen eingeben kann.

Um zu einer neuen Ansicht zu gelangen, benötigen Sie einen Router. Erstellen Sie eine neue Swift-Datei mit dem Namen TripDetailRouter.rasch.

Fügen Sie diesen Code zur neuen Datei hinzu:

import SwiftUIclass 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) }}

Dadurch wird ein WaypointView erstellt, das bereits eingerichtet und einsatzbereit ist.

Gehen Sie mit dem Router zu TripDetailInteractor.swift, und fügen Sie die folgenden Methoden:

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}

Diese Methoden sind selbstbeschreibend. Sie fügen Wegpunkte hinzu, verschieben, löschen und aktualisieren sie.

Als nächstes setzen Sie diese der Ansicht durch TripDetailPresenter aus. Fügen Sie in TripDetailPresenter diese Eigenschaft hinzu:

private let router: TripDetailRouter

Dies hält den Router. Erstellen Sie es, indem Sie dies oben in init(interactor:) hinzufügen:

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

Dadurch wird der Router für die Verwendung mit dem Wegpunkteditor erstellt. Fügen Sie als Nächstes diese Methoden hinzu:

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) }}

Die ersten drei sind Teil der Operationen auf dem Wegpunkt. Die letzte Methode ruft den Router auf, um eine Wegpunktansicht für den Wegpunkt abzurufen und in eine NavigationLink .

Zeigen Sie dies schließlich dem Benutzer in TripDetailView, indem Sie Folgendes zur VStack unter der Text hinzufügen:

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

Dadurch werden der Ansicht die folgenden Steuerelemente hinzugefügt:

  • Ein EditButton, der die Liste in den Bearbeitungsmodus versetzt, sodass der Benutzer Wegpunkte verschieben oder löschen kann.
  • Ein add Button, das den Presenter verwendet, um einen neuen Wegpunkt zur Liste hinzuzufügen.
  • Eine List, die eine ForEach mit dem Presenter verwendet, um eine Zelle für jeden Wegpunkt zu erstellen. Die Liste definiert eine onMove und onDelete Aktion, die diese Bearbeitungsaktionen aktiviert und in den Presenter zurückruft.

Erstellen und ausführen, und Sie können jetzt eine Reise anpassen! Achten Sie darauf, alle Änderungen zu speichern.

Dem Detailbildschirm hinzugefügte Wegpunkte
Der Wegpunkteditor

Module erstellen

Mit VIPER können Sie Presenter, Interactor, View, Router und zugehörigen Code in Modulen zusammenfassen.

Traditionell würde ein Modul die Schnittstellen für Presenter, Interactor und Router in einem einzigen Vertrag verfügbar machen. Dies macht bei SwiftUI nicht viel Sinn, da es nach vorne gerichtet ist. Wenn Sie nicht jedes Modul als eigenes Framework verpacken möchten, können Sie Module stattdessen als Gruppen konzipieren.

Nehmen Sie TripListView.schnell, TripListPresenter.schnell, TripListInteraktor.swift und TripListRouter.swift und gruppieren Sie sie in einer Gruppe namens TripListModule.

Machen Sie dasselbe für die Detailklassen: TripDetailView .schnell, TripDetailPresenter.schnell, TripDetailInteractor.schnell, TripMapViewPresenter.schnell, TripMapView.swift und TripDetailRouter.rasch.

Fügen Sie sie einer neuen Gruppe namens TripDetailModule hinzu.

Module sind eine gute Möglichkeit, den Code sauber und getrennt zu halten. Als Faustregel sollte ein Modul ein konzeptioneller Bildschirm / eine konzeptionelle Funktion sein, und die Router übergeben den Benutzer zwischen den Modulen.

Wohin von hier aus?

Klicken Sie oben oder unten im Tutorial auf die Schaltfläche Materialien herunterladen, um die fertigen Projektdateien herunterzuladen.

Einer der Vorteile der Trennung, die VIPER befürwortet, liegt in der Testbarkeit. Sie können den Interaktor testen, damit er das Datenmodell lesen und bearbeiten kann. Und das alles können Sie tun, während Sie den Presenter unabhängig testen, um die Ansicht zu ändern und auf Benutzeraktionen zu reagieren.

Betrachten Sie es als eine lustige Übung, um es selbst auszuprobieren!

Aufgrund der Blindleistung von Combine und seiner nativen Unterstützung in SwiftUI haben Sie möglicherweise bemerkt, dass die Ebenen Interactor und Presenter relativ dünn sind. Sie trennen die Bedenken, aber meistens übergeben sie nur Daten durch eine Abstraktionsschicht.

Mit SwiftUI ist es etwas natürlicher, die Presenter- und Interactor-Funktionalität in einem einzigen ObservableObject das den größten Teil des Ansichtsstatus enthält und direkt mit den Entitäten interagiert.

Für einen alternativen Ansatz, lesen Sie MVVM mit Combine Tutorial für iOS.

Wir hoffen euch hat dieses Tutorial gefallen! Wenn Sie an Fragen oder Kommentare denken, lassen Sie sie in der folgenden Diskussion fallen. Wir würden uns freuen, von Ihrer Lieblingsarchitektur zu hören und was sich in der Ära von SwiftUI geändert hat.

raywenderlich.com Wöchentlich

Die raywenderlich.com newsletter ist der einfachste Weg, um über alles auf dem Laufenden zu bleiben, was Sie als mobiler Entwickler wissen müssen.

Holen Sie sich einen wöchentlichen Überblick über unsere Tutorials und Kurse und erhalten Sie als Bonus einen kostenlosen ausführlichen E-Mail-Kurs!

Durchschnittliche Bewertung

4.6/5

Bewertung für diesen Inhalt hinzufügen

Zum Hinzufügen einer Bewertung anmelden

33 Bewertungen

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.