Aan de slag met het Viper-Architectuurpatroon

het Viper-architectuurpatroon is een alternatief voor MVC of MVVM. En terwijl de SwiftUI en Combine frameworks zorgen voor een krachtige combinatie die snel werk maakt van het bouwen van complexe UI ‘ s en het verplaatsen van gegevens rond een app, komen ze ook met hun eigen uitdagingen en meningen over architectuur.

Het is een algemene overtuiging dat alle van de app logica moet nu gaan in een SwiftUI uitzicht, maar dat is niet het geval.

VIPER biedt een alternatief voor dit scenario en kan worden gebruikt in combinatie met SwiftUI en combineren om apps te bouwen met een schone architectuur die effectief de verschillende functies en verantwoordelijkheden scheidt die nodig zijn, zoals de gebruikersinterface, bedrijfslogica, gegevensopslag en netwerken. Deze zijn dan gemakkelijker te testen, te onderhouden en uit te breiden.

in deze tutorial, zult u een app bouwen met behulp van het Viper architectuur patroon. De app wordt ook gunstig genoemd VIPER: visueel interessante geplande gemakkelijke roadtrip. Slim, toch? :]

Het stelt gebruikers in staat om roadtrips uit te bouwen door waypoints toe te voegen aan een route. Onderweg leer je ook over SwiftUI en combineer je voor je iOS-projecten.

Viper app hoofdscherm

aan de slag

Download de projectmaterialen van de Download materialen knop aan de boven-of onderkant van de tutorial. Open het starter project. Dit bevat enkele code om u op weg te helpen:

  • De ContentView zal de andere weergaven van de app starten terwijl u ze bouwt.
  • Er zijn enkele hulpweergaven in de groep functionele weergaven: een voor het afbreken van de MapKit-kaartweergave, een speciale “gesplitste afbeelding” – weergave, die wordt gebruikt door de TripListCell. Je voegt deze zo aan het scherm toe.
  • in de groep entiteiten ziet u de klassen gerelateerd aan het datamodel. Trip en Waypoint zullen later dienen als de entiteiten van de Viper architectuur. Als zodanig houden ze alleen gegevens vast en bevatten ze geen functionele logica.
  • in de groep gegevensbronnen zijn er de helperfuncties voor het opslaan of laden van gegevens.
  • Gluur vooruit als je wilt in de WaypointModule groep. Dit heeft een Viper implementatie van het Waypoint editing scherm. Het is opgenomen met de starter, zodat u de app kunt voltooien tegen het einde van deze tutorial.

Dit voorbeeld maakt gebruik van een site voor het delen van foto ‘ s met een vergunning. Om afbeeldingen naar de app te halen, moet je een GRATIS account aanmaken en een API-sleutel verkrijgen.

Volg hier de instructies https://.com/accounts/register/ om een account aan te maken. Kopieer vervolgens uw API-sleutel naar de apiKey variabele gevonden in ImageDataProvider.snel. U kunt het vinden in de API docs onder Zoek afbeeldingen.

Als u nu bouwt en uitvoert, zult u niets interessants zien.

VIPER app bij starter project

echter, aan het einde van de tutorial, heb je een volledig functionele roadtrip planning app.

Wat is VIPER?

VIPER is een architecturaal patroon zoals MVC of MVVM, maar het scheidt de code verder door enkele verantwoordelijkheid. Apple-stijl MVC motiveert ontwikkelaars om alle logica in een UIViewController subklasse te plaatsen. VIPER, net als MVVM ervoor, probeert dit probleem op te lossen.

elk van de letters in VIPER staat voor een component van de architectuur: weergave, Interactor, presentator, entiteit en Router.

  • De weergave is de gebruikersinterface. Dit komt overeen met een SwiftUI View.
  • de Interactor is een klasse die bemiddelt tussen de presentator en de data. Het neemt de richting van de presentator.
  • de presentator is de “traffic cop” van de architectuur, waarbij gegevens tussen de weergave en de interactor worden geleid, gebruikersacties worden uitgevoerd en naar de router wordt gebeld om de gebruiker tussen weergaven te verplaatsen.
  • een entiteit vertegenwoordigt toepassingsgegevens.
  • de Router behandelt navigatie tussen schermen. Dat is anders dan het is in SwiftUI, waar het uitzicht toont elke nieuwe standpunten.

deze scheiding komt voort uit het schone Architectuurparadigma van” Oom ” Bob Martin.

VIPER Diagram

wanneer u naar het diagram kijkt, kunt u zien dat er een volledig pad is voor de gegevensstroom tussen de weergave en entiteiten.

SwiftUI heeft zijn eigen eigenzinnige manier van doen. De toewijzing van Viper verantwoordelijkheden op domein objecten zal anders zijn als je dit vergelijkt met tutorials voor UIKit apps.

het vergelijken van architecturen

mensen bespreken VIPER vaak met MVC en MVVM, maar het verschilt van die patronen.

MVC, of Model-View-Controller, is het patroon dat de meeste mensen associëren met de iOS-apparchitectuur van 2010. Met deze aanpak definieer je de weergave in een storyboard, en de Controller is een geassocieerde UIViewController subklasse. De Controller wijzigt de weergave, accepteert gebruikersinvoer en communiceert rechtstreeks met het Model. De Controller zwelt op met view logic en business logic.

MVVM is een populaire architectuur die de view logic scheidt van de business logic in een View Model. Het view model werkt samen met het Model.

het grote verschil is dat een weergavemodel, in tegenstelling tot een weergavecontroller, slechts een eenrichtingsreferentie heeft naar de weergave en naar het model. MVVM is een goede pasvorm voor SwiftUI, en er is een hele tutorial over het onderwerp.

VIPER gaat een stap verder door het scheiden van de weergave logica van de data model logica. Alleen de presentator praat met het uitzicht, en alleen de interactor praat met het model (entiteit). De presentator en de interactor coördineren met elkaar. De presentator houdt zich bezig met weergave en gebruikersactie, en de interactor houdt zich bezig met het manipuleren van de gegevens.

een Adderslang, voor de lol

Defining an Entity

VIPER is een leuk acroniem voor deze architectuur, maar de volgorde is niet proscriptief.

de snelste manier om iets op het scherm te krijgen is door te beginnen met de entiteit. De entiteit is het(de) gegevensobject (en) voor het project. In dit geval zijn de belangrijkste entiteiten Trip, die een lijst van Waypoints bevat, die de stops in de reis zijn.

De app bevat een datamodel klasse die een lijst van trips bevat. Het model gebruikt een JSON bestand voor lokale persistentie, maar je kunt dit vervangen door een remote back-end zonder dat je een van de UI-level code hoeft aan te passen. Dat is een van de voordelen van een schone architectuur: als je een deel wijzigt — zoals de persistentielaag — wordt het geïsoleerd van andere delen van de code.

een Interactor toevoegen

Maak een nieuw Swift-bestand met de naam TripListInteractor.snel.

voeg de volgende code toe aan het bestand:

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

hiermee maakt u de interactorklasse aan en kent u deze een DataModel toe, die u later zult gebruiken.

de presentator instellen

Maak nu een nieuw Swift-bestand aan met de naam TripListPresenter.snel. Dit is voor de presentatorklas. De presentator geeft om het verstrekken van gegevens aan de gebruikersinterface en het bemiddelen van gebruikersacties.

voeg deze code toe aan het bestand:

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

Dit maakt een presenter-klasse aan die verwijst naar de interactor.

omdat het de taak van de presentator is om de weergave met gegevens te vullen, wilt u de lijst met trips uit het datamodel tonen.

voeg een nieuwe variabele toe aan de klasse:

@Published var trips: = 

Dit is de lijst met trips die de gebruiker in de weergave zal zien. Door het te declareren met de@Published property wrapper, kan de weergave naar wijzigingen in de property luisteren en zichzelf automatisch bijwerken.

de volgende stap is om deze lijst te synchroniseren met het gegevensmodel van de interactor. Voeg eerst de volgende helper-eigenschap toe:

private var cancellables = Set<AnyCancellable>()

Deze set is een plaats om combineerabonnementen op te slaan, zodat hun levensduur is gekoppeld aan die van de klasse. op die manier blijven alle abonnementen actief zolang de presentator in de buurt is.

voeg de volgende code toe aan het einde van init(interactor:):

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

interactor.model.$trips creëert een uitgever die wijzigingen in de trips collectie van het gegevensmodel bijhoudt. De waarden worden toegewezen aan de eigen trips collectie van deze klasse, waarmee een link wordt gemaakt die de trips van de presentator bijgewerkt houdt wanneer het gegevensmodel verandert.

ten slotte wordt dit abonnement opgeslagen in cancellables zodat u het later kunt opschonen.

een weergave bouwen

u moet nu de eerste weergave bouwen: de weergave van de reislijst.

een weergave aanmaken met een presentator

Maak een nieuw bestand aan vanuit het SwiftUI View-sjabloon en noem het TripListView.snel.

voeg de volgende eigenschap toe aan TripListView:

@ObservedObject var presenter: TripListPresenter

Dit koppelt de presentator aan de weergave. Repareer vervolgens de previews door de body van TripListView_Previews.previews te veranderen naar:

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

vervang nu de inhoud van TripListView.body door:

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

Dit maakt een List waar de trips van de presentator worden opgesomd, en het genereert een vooraf opgegeven TripListCell voor elk.

voorbeeldvenster van de weergave van de ritlijst

het Model wijzigen vanuit de weergave

tot nu toe hebt u gegevens zien stromen van de entiteit naar de interactor via de presentator om de weergave te vullen. Het VIPER-patroon is nog nuttiger wanneer het verzenden van gebruikersacties terug naar beneden om het gegevensmodel te manipuleren.

om dat te zien, voegt u een knop toe om een nieuwe trip aan te maken.

voeg eerst het volgende toe aan de klasse in TripListInteractor.snel:

func addNewTrip() { model.pushNewTrip()}

Dit wraps pushNewTrip(), die een nieuwe Trip aanmaakt boven aan de trips-lijst.

dan, in TripListPresenter.swift, voeg dit toe aan de klasse:

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

Dit maakt een knop aan met het systeem + image met een actie die addNewTrip()aanroept. Dit stuurt de actie naar de interactor, die het gegevensmodel manipuleert.

Ga terug naar TripListView.swift en voeg het volgende toe na de List :

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

hiermee voegt u de knop en een titel toe aan de navigatiebalk. Wijzig nu de return in TripListView_Previews als volgt:

return NavigationView { TripListView(presenter: presenter)}

Hiermee kunt u de navigatiebalk in voorbeeldmodus zien.

hervat het live voorbeeld om de knop te zien.

Triplijst met knop in Live Preview

het in actie zien

nu is het een goed moment om terug te gaan en TripListView over te zetten naar de rest van de toepassing.

Open ContentView.snel. In de body van view, vervang de VStack door:

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

Dit maakt de weergave samen met zijn presentator en interactor. Nu bouwen en rennen.

tikken op de + knop zal een nieuwe Trip toevoegen aan de lijst.

Trip Lijst met een nieuwe Trip toegevoegd

het verwijderen van een Trip

gebruikers die trips maken zullen waarschijnlijk ook in staat willen zijn om ze te verwijderen in het geval ze een fout maken of wanneer de trip voorbij is. Nu u het gegevenspad hebt gemaakt, is het toevoegen van extra acties aan het scherm eenvoudig.

in TripListInteractor, voeg toe:

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

Dit verwijdert items uit de trips verzameling in het gegevensmodel. Omdat het een@Published eigenschap heeft, zal de gebruikersinterface automatisch worden bijgewerkt vanwege zijn abonnement op de wijzigingen.

in TripListPresenter, voeg toe:

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

Dit stuurt het delete Commando door naar de interactor.

ten slotte, inTripListView, voeg het volgende toe na de eindsteun van de ForEach:

.onDelete(perform: presenter.deleteTrip)

het toevoegen van een .onDelete aan een item in een SwiftUI List schakelt automatisch de swipe in om gedrag te verwijderen. De actie wordt vervolgens naar de presentator gestuurd, waardoor de hele keten wordt afgetrapt.

bouwen en uitvoeren, en je zult nu in staat zijn om trips te verwijderen!

met onDelete is de actie Verwijderen ingeschakeld.

routering naar de detailweergave

nu is het tijd om het Routergedeelte Van VIPER toe te voegen.

een router stelt de gebruiker in staat om vanuit de weergave van de ritlijst naar de weergave van de ritdetails te navigeren. De rit detailweergave toont een lijst van de waypoints samen met een kaart van de route.

De gebruiker kan de lijst met waypoints en de naam van de reis bewerken vanuit dit scherm.

Yay Router!

het instellen van de Trip detail schermen

voordat u het detail scherm toont, moet u het maken.

maak in het vorige voorbeeld twee nieuwe Swift-bestanden aan: TripDetailPresenter.swift en TripDetailInteractor.swift en een SwiftUI View genaamd TripDetailView.snel.

Stel de inhoud van TripDetailInteractor in op:

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

Dit maakt een nieuwe klasse aan voor de interactor van het trip detail scherm. Dit interageert met twee gegevensbronnen: een individuele Trip en kaartinformatie van MapKit. Er is ook een set voor de annulerbare abonnementen die u later zult toevoegen.

stel vervolgens in TripDetailPresenter de inhoud in op:

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

Dit maakt een stub-presentator met een referentie voor interactor en annuleerbare set. Je bouwt dit zo op.

in TripDetailView, voeg de volgende eigenschap toe:

@ObservedObject var presenter: TripDetailPresenter

Dit voegt een verwijzing toe naar de presentator in de weergave.

om de previews opnieuw te laten bouwen, verander je die stub naar:

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

nu wordt de weergave gebouwd, maar de preview is nog steeds ” Hallo, wereld!”

alleen het standaardweergavevoorbeeld

Routing

voordat u de detailweergave opbouwt, wilt u deze koppelen aan de rest van de app via een router uit de reislijst.

Maak een nieuw Swift-bestand aan met de naam TripListRouter.snel.

De inhoud instellen op:

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

Deze klasse geeft een nieuwe TripDetailView die is ingevuld met een interactor en presentator. De router behandelt de overgang van het ene scherm naar het andere, het instellen van de klassen die nodig zijn voor de volgende weergave.

In een imperatief UI paradigma-met andere woorden, met UIKit-zou een router verantwoordelijk zijn voor het presenteren van weergavecontrollers of het activeren van segues.

SwiftUI declareert alle doelweergaven als onderdeel van de huidige weergave en toont ze op basis van weergavestatus. Om Viper op SwiftUI in kaart te brengen, is de weergave nu verantwoordelijk voor het tonen/verbergen van weergaven, de router is een bestemmingsweergavebouwer en de presentatorcoördinaten ertussen.

in TripListPresenter.swift, voeg de router toe als een eigenschap:

private let router = TripListRouter()

u hebt de router nu aangemaakt als onderdeel van de presenter.

voeg vervolgens deze methode toe:

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

Dit maakt een NavigationLink aan een detailweergave die de router biedt. Wanneer u het in een NavigationView plaatst, wordt de link Een knop die de destination op de navigatie stack drukt.

het content blok kan een willekeurige SwiftUI-weergave zijn. Maar in dit geval geeft TripListView een TripListCell.

Ga naar TripListView.swift en wijzig de inhoud van de ForEach naar:

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

Dit gebruikt de NavigationLink van de presentator, stelt de cel in als de inhoud en plaatst deze in de lijst.

bouwen en uitvoeren, en nu, wanneer de gebruiker de cel tikt, zal het ze doorsturen naar een “Hello World” TripDetailView.

detailscherm Hello World

de detailweergave afronden

Er zijn nog enkele ritdetails die u moet invullen om de detailweergave te kunnen bekijken en de routepunten te kunnen bewerken.

begin met het toevoegen van de titel van de trip:

In TripDetailInteractor, voeg de volgende eigenschappen toe:

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

Dit toont alleen de String versie van de trip naam en een Publisher voor wanneer die naam verandert.

voeg ook het volgende toe:

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

met de eerste methode kan de presentator de naam van de trip wijzigen, en de tweede zal het model opslaan in de persistence layer.

ga nu naar TripDetailPresenter. Voeg de volgende eigenschappen toe:

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

Deze bieden de haken voor de weergave om de naam van de trip te lezen en in te stellen.

voeg vervolgens het volgende toe aan de init methode:

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

deze code:

  1. maakt een binding aan om de naam van de trip in te stellen. De TextField zal dit gebruiken in de weergave om te kunnen lezen en schrijven van de waarde.
  2. wijst de naam van de trip van de uitgever van de interactor toe aan de eigenschap tripName van de presentator. Dit houdt de waarde gesynchroniseerd.

door de naam van de trip te scheiden in eigenschappen zoals deze kunt u de waarde synchroniseren zonder een oneindige lus van updates te creëren.

voeg vervolgens dit toe:

func save() { interactor.save()}

Dit voegt een opslagfunctie toe zodat de gebruiker alle bewerkte details kan opslaan.

ten slotte, ga naar TripDetailView, en vervang de body door:

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

de VStack bevat nu een TextField voor het bewerken van de naam van de reis. De navigatiebalkmodifiers definiëren de titel met behulp van de gepubliceerde tripName van de presentator, zodat het wordt bijgewerkt als het gebruikerstype, en een knop Opslaan Die eventuele wijzigingen zal aanhouden.

bouwen en uitvoeren, en nu kunt u de titel van de trip bewerken.

Bewerk de naam in de detailweergave

Sla op na het bewerken van de naam van de trip, en de wijzigingen zullen verschijnen nadat u de app opnieuw start.

veranderingen blijven bestaan na het opslaan van

met behulp van een tweede presentator voor de kaart

het toevoegen van extra widgets aan een scherm zal hetzelfde patroon volgen als:

  • Het toevoegen van functionaliteit aan de interactor.
  • overbruggen van de functionaliteit via de presentator.
  • widgets toevoegen aan de weergave.

Ga naar TripDetailInteractor, en voeg de volgende eigenschappen toe:

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

Deze geven de volgende informatie over de waypoints in een trip: de totale afstand als een Measurement, de lijst van waypoints en een lijst van richtingen die deze verbinden waypoints.

voeg vervolgens de volgende abonnementen toe aan het einde van 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<, Never>() } .assign(to: \.directions, on: self) .store(in: &cancellables)

Dit voert drie afzonderlijke acties uit op basis van het wijzigen van de routepunten van de reis.

de eerste is slechts een kopie naar de lijst met waypoints van de interactor. De tweede gebruikt de mapInfoProvider om de totale afstand voor alle waypoints te berekenen. En de derde gebruikt dezelfde data provider om routebeschrijvingen te krijgen tussen de waypoints.

de presentator gebruikt deze waarden om informatie te verstrekken aan de gebruiker.

Ga naar TripDetailPresenter, en voeg deze eigenschappen toe:

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

De weergave zal deze eigenschappen gebruiken. Wire ze up voor het bijhouden van gegevens wijzigingen door het volgende toe te voegen aan het einde van 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)

het eerste abonnement neemt de ruwe afstand van de interactor en formatteert deze voor weergave in de weergave, en de tweede kopieert alleen over de waypoints.

rekening houdend met de kaartweergave

voordat u naar de detailweergave gaat, moet u de kaartweergave overwegen. Deze widget is ingewikkelder dan de andere.

naast het tekenen van de geografische kenmerken, overlapt de app ook pinnen voor elk punt en de route ertussen.

dit vraagt om een eigen set van presentatie logica. U kunt de TripDetailPresenter gebruiken, of in dit geval een aparte TripMapViewPresenteraanmaken. Het zal de TripDetailInteractor hergebruiken omdat het hetzelfde gegevensmodel deelt en een alleen-lezen weergave is.

Maak een nieuw Swift-bestand aan met de naam TripMapViewPresenter.snel. Stel de inhoud in op:

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 toont de kaartpresentator twee arrays voor annotaties en routes. In init(interactor:), kunt u de waypoints van de interactor toewijzen aan MKPointAnnotation objecten zodat ze als pins op de kaart kunnen worden weergegeven. U kopieert vervolgens dedirections naar deroutes array.

om de presentator te gebruiken, maakt u een nieuwe SwiftUI-weergave aan met de naam TripMapView.snel. Stel de inhoud in op:

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

Dit gebruikt de helper MapView en levert het met pins en routes van de presentator. De previews struct bouwt de VIPER keten die de app nodig heeft om alleen de kaart te bekijken. Gebruik Live Preview om de kaart goed te zien:

voorbeeldvenster met TripMapView

om de kaart aan de app toe te voegen, voegt u eerst de volgende methode toe aan TripDetailPresenter:

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

Dit maakt een kaartweergave, die wordt voorzien van de presentator.

open vervolgens TripDetailView.snel.

voeg het volgende toe aan de VStack onder de TextField:

presenter.makeMapView()Text(presenter.distanceLabel)

Build and run to see the map on screen:

kaartweergave werken in de app

Waypoints bewerken

de laatste functie is het toevoegen van waypoints bewerken zodat u uw eigen trips kunt maken! U kunt de lijst opnieuw rangschikken in de weergave reisdetails. Maar om een nieuw waypoint te maken, heb je een nieuwe weergave nodig voor de gebruiker om de naam in te typen.

om een nieuwe weergave te krijgen, hebt u een Router nodig. Maak een nieuw Swift-bestand met de naam TripDetailRouter.snel.

voeg deze code toe aan het nieuwe bestand:

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

hiermee wordt een WaypointView aangemaakt die al is ingesteld en klaar is voor gebruik.

met de router bij de hand, ga naar TripDetailInteractor.swift, en voeg de volgende methoden toe:

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}

deze methoden zijn zelfbeschrijvend. Ze voegen waypoints toe, verplaatsen, verwijderen en bijwerken.

Toon deze vervolgens aan de weergave via TripDetailPresenter. Voeg in TripDetailPresenter deze eigenschap toe:

private let router: TripDetailRouter

Dit houdt de router vast. Maak het aan door dit toe te voegen aan de bovenkant van init(interactor:):

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

hiermee wordt de router gemaakt voor gebruik met de waypoint-editor. Voeg vervolgens deze methoden toe:

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

de eerste drie maken deel uit van de bewerkingen op het waypoint. De laatste methode roept de router aan om een waypoint-weergave voor het waypoint te krijgen en zet het in een NavigationLink.

ten slotte, toon dit aan de gebruiker in TripDetailView door het volgende toe te voegen aan de VStack onder de Text:

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(_:))}

Dit voegt de volgende besturingselementen toe aan de weergave:

  • an EditButton die de lijst in bewerkingsmodus zet zodat de gebruiker waypoints kan verplaatsen of verwijderen.
  • an add Button die de presentator gebruikt om een nieuw waypoint aan de lijst toe te voegen.
  • A List die een ForEach gebruikt met de presentator om een cel te maken voor elk waypoint. De lijst definieert eenonMove enonDelete actie die deze bewerkingen mogelijk maakt en terugroept naar de presentator.

bouwen en uitvoeren, en u kunt nu een reis aanpassen! Zorg ervoor dat u eventuele wijzigingen op te slaan.

Waypoints toegevoegd aan detailscherm
de waypoint Editor

Making Modules

met VIPER kunt u de presentator, interactor, weergave, router en gerelateerde code groeperen in modules.

traditioneel zou een module de interfaces voor de presentator, interactor en router blootstellen in een enkel contract. Dit heeft niet veel zin met SwiftUI, want het is vooruitzicht. Tenzij u elke module als een eigen framework wilt verpakken, kunt u in plaats daarvan modules als groepen conceptualiseren.

neem TripListView.swift, TripListPresenter.snelle, TripListInteractor.swift en TripListRouter.swift en groepeer ze samen in een groep genaamd TripListModule.

doe hetzelfde voor de detailklassen: TripDetailView.swift, TripDetailPresenter.swift, TripDetailInteractor.swift, TripMapViewPresenter.swift, TripMapView.snel, en TripDetailRouter.snel.

voeg ze toe aan een nieuwe groep genaamd TripDetailModule.

Modules zijn een goede manier om de code schoon en gescheiden te houden. Als een goede vuistregel moet een module een conceptueel scherm/functie zijn, en de routers geven de gebruiker tussen modules.

waar moet ik nu naartoe?

klik op de knop materialen Downloaden boven of onder in de tutorial om de voltooide projectbestanden te downloaden.

een van de voordelen van de scheiding Viper endores is in testbaarheid. Je kunt de interactor testen zodat hij het datamodel kan lezen en manipuleren. En je kunt dat allemaal doen terwijl je de presentator onafhankelijk test om de weergave te wijzigen en te reageren op acties van de gebruiker.

zie het als een leuke oefening om alleen te proberen!

vanwege het reactieve vermogen van Combine en zijn eigen ondersteuning in SwiftUI, is het u misschien opgevallen dat de lagen interactor en presentator relatief dun zijn. Ze scheiden de zorgen, maar meestal geven ze alleen gegevens door via een abstractielaag.

met SwiftUI is het iets natuurlijker om de functionaliteit van de presentator en de interactor samen te voegen tot een enkele ObservableObject die het grootste deel van de weergavestatus bevat en rechtstreeks met de entiteiten communiceert.

voor een alternatieve aanpak, lees MVVM met combineer Tutorial voor iOS.

We hopen dat je genoten hebt van deze tutorial! Als u denkt aan vragen of opmerkingen, laat ze vallen in de discussie hieronder. We horen graag over uw favoriete architectuur en wat er is veranderd in het tijdperk van SwiftUI.

raywenderlich.com wekelijks

De raywenderlich.com nieuwsbrief is de makkelijkste manier om op de hoogte te blijven van alles wat u als mobiele ontwikkelaar moet weten.

ontvang een wekelijkse samenvatting van onze tutorials en cursussen, en ontvang een gratis diepgaande e-mail cursus als bonus!

gemiddelde waardering

4.6/5

voeg een waardering voor deze inhoud toe

Meld u aan om een waardering toe te voegen

33 beoordelingen

Geef een antwoord

Het e-mailadres wordt niet gepubliceerd.