VIPER er komplekst, skræmmende og – ja – anderledes. Der er en antydning af gift under overfladen af VIPER, og det kan gøre ondt. Men med tiden aftager smerten, og det bliver en af kuren mod strukturering af din kode. Farvel, farvel til giften. Hej lykke.
Her er noget, som iOS-udviklere kan forholde sig til:
Designmønsteret VIPER er en kilde til forvirring & misforståelse for mange udviklere. Fordelene & ulemperne og den praktiske anvendelse af dette mønster er uklare. Efter at have arbejdet med applikationer med MVC-, MVVM-, MVVM- og MVVM + RxSwift-mønstre ønskede jeg at få en dybdegående forståelse af dette ondskabsfuldt klingende designmønster og se, hvad buzzen (hvæsen?) gik ud på. Som følge heraf besluttede jeg mig for at teste følgende hypotese:
Som udvikler vil jeg gerne bruge VIPER-designmønsteret til at opbygge genanvendelige moduler.
Vi skrev dette indlæg i håb om, at det sætter dig i stand til at vurdere, om VIPER er den rigtige løsning for dit projekt eller din ramme.
VIPER
Et par folk ovre hos Mutual Mobile ønskede at forbedre deres testdækning på deres iOS-apps. Som følge heraf fandt de på selve mønstret. Du kan finde mere om, hvor det kom fra, her.
Litterært set er Viper et backronym, der står for View, Interactor, Presenter, Entity, Router.
Theoretisk set, mens du bruger Viper, vil din applikation eller dit framework blive opdelt i VIPER-“moduler” – genanvendelige & modulkomponenter. I diagramform kan det oversættes til følgende:
View:
Interfacelaget udlægger visningen, opdaterer billeder og etiketter, videregiver brugerinput til præsentatoren
Interaktør: Forretningslogik, manipulation af data, transformation af modeller, interagerer med API-tjenester & datamanagerklasser. Videregiver output til præsentatoren
Presenter: Formaterer data modtaget fra interaktøren til visningen, modtager input fra visningen og videregiver det til interaktøren
Entitet: En simpel data, der repræsenterer det, der er nødvendigt for, at VIPER-modulet kan fungere
Router: Ansvarlig for modulnavigation og applikationsnavigation
VIPER i kode fungerer ikke i samme rækkefølge som de bogstaver, du læser. Med andre ord … handlingerne i din kode vil flyde mere sådan i nedenstående rækkefølge:
Interaktør → Router , Entity, & Eksternt → Presenter → View
VIPER bliver mere som IREPV… Det er et ord til nogen, et eller andet sted.
EXPERIMENT
Vi validerede hypotesen om at bruge VIPER i et genanvendeligt modul ved at bygge en ramme. Rammen er fokuseret på afspilning af videoindhold. Afspilning af video indebærer UI-opdateringer, dataoverførsel & dataoverførsel & datasynkronisering. Disse interaktioner beviste, at et videoafspilningsmodul ville være en værdifuld kandidat til et VIPER-struktureret modul.
OVERVIEW
VIPER-moduler er protokolorienterede. Hver funktion, egenskab, indgang og udgang er implementeret via en protokol.
Som følge heraf brugte vi ideen om en:
Kontrakt: en swift-fil, der beskriver alle protokoller, der anvendes i VIPER-modulet. Den svarer til en objective-C header-fil. Kodeuddrag nedenfor:
//*** Interactor//*//*protocol VPKVideoPlaybackInteractorProtocol: class {
init(entity: VPKVideoType)}
protocol VPKVideoPlaybackInteractorInputProtocol: class {
var presenter: VPKVideoPlaybackInteractorOutputProtocol? { get set }var videoType: VPKVideoType { get set }var remoteImageURL: URL? { get set }var localImageName: String? { get set }
var playbackManager: (VPKVideoPlaybackManagerInputProtocol &VPKVideoPlaybackManagerOutputProtocol &VPKVideoPlaybackManagerOutputProtocol &VPKVideoPlaybackManagerProtocol)? { get set }
// PRESENTER -> INTERACTORfunc didTapVideo(videoURL: URL, at indexPath: NSIndexPath?)func didScrubTo(_ timeInSeconds: TimeInterval)func didSkipBack(_ seconds: Float)func didSkipForward(_ seconds: Float)func didToggleViewExpansion()func didMoveMoveOffScreen()func didReuseInCell()}
Vi ønskede også, at udviklere af denne ramme skulle have en nem måde at konfigurere VIPER-moduler på.
Som et resultat heraf brugte vi ideen om en:
Builder: Et simpelt objekt, der bruges til at konfigurere VIPER-modulet og opsætte afhængigheder. Det instantierer præsentatoren og interaktøren. I sidste ende returnerer den visningen til viewcontrolleren.
public class VPKVideoPlaybackBuilder: VPKVideoPlaybackBuilderProtocol {
//Feedpublic static func vpk_buildViewInCell(for videoType:VPKVideoType, at indexPath: NSIndexPath, med playbackBarTheme:ToolBarTheme = .normal, completion viewCompletion:VideoViewClosure) {
let interactor = VPKVideoPlaybackInteractor(entity:videoType)let presenter: VPKVideoPlaybackPresenterProtocol &VPKVideoPlaybackInteractorOutputProtocol =VPKVideoPlaybackPresenter(with: false, showInCell: nil,playbackTheme: playbackBarTheme)viewCompletion(VPKDependencyManager.videoView(with:interactor, and: presenter))
}}
Vi brugte også en protokol til at oprette alle afhængigheder for modulet:
class VPKDependencyManager: VPKDependencyManagerProtocol {
static func videoView(with interactor:VPKVideoPlaybackInteractorProtocol &VKVPKVPideoPlaybackInteractorInputProtocol, and presenter:VPKVideoPlaybackPresenterProtocol &VKVPKVPideoPlaybackInteractorOutputProtocol) -> VPKVideoView {
let videoView = VPKVideoView(frame: .zero)let playbackBarView: VPKPlaybackControlViewProtocol =VPKPlaybackControlView(theme: presenter.playbackTheme ?? .normal)let videoPlaybackManager: VPKVideoPlaybackManagerInputProtocol &VPKVideoPlaybackManagerOutputProtocol &VPKVideoPlaybackManagerProtocol = VPKVideoPlaybackManager.shared//Dependency setuppresenter.interactor = interactorinteractor.presenter = presentervideoView.presenter = presentervideoView.playbackBarView = playbackBarViewpresenter.playbackBarView = playbackBarViewplaybackBarView.presenter = presenterpresenter.videoView = videoViewinteractor.playbackManager = videoPlaybackManagerreturn videoView
}}
THE FRAMEWORK: VIDEOPLAYBACKKIT
Lad os overveje brugerhistorien:
**PLAY A VIDEO **
Ved hjælp af AVFOUNDATION ved vi følgende:
- Måtte bruge AVPlayer
- AVPlayer giver dig et AVPlayerLayer, som du kan tilføje til din visning
- AVPlayer tager tid til at forberede – især med fjernvideo-urls
- Fjernvideodata skal have tid til at downloade
-
To primære visningshandlinger
- Play
- Pause
-
Resultater af disse handlinger:
- AVPlayer modtager video url til download af data for
- Når den er klar, returnerer AVPlayer et AVPlayerLayer
- AvPlayerLayer skal tilføjes til en visning
- På pause skal AVPlayer stoppe afspilning
**IREPV (AKA VIPER) & AVKit: & AVKit, AVFoundation **
INPUTS OG OUTPUTS
Nu er dette nøglen til at forstå mønsteret, berolige din hjerne, undgå at falde ned i kaninhul kompleksiteter.
Hovedlektion 1: Forstå ind- og udgange i dit modul.
View
- Input: didTapPlay
- Output: Presenter didTapPlay
Presenter
- Input: didTapPlay modtager fra view
- Output: onPlayerLayerSuccess(playerLayer) – udsender dette til presenter
Interactor
- Input: didTapPlay modtager fra view
- Output: onPlayerLayerSuccess(playerLayer) – udsender dette til presenter
Interactor
- Input: didTapPlay – fortæller den eksterne manager
-
Output: onVideoPlayerLayerSuccess – videregiver playerlag til præsentatoren
protokol VPKVideoPlaybackInterteractorOutputProtocol: class {
var progressTime: TimeInterval { get set }
//INTERACTOR –> PRESENTERfunc onVideoResetPresentation()func onVideoLoadSuccess(_ playerLayer: AVPlayerLayer)func onVideoLoadFail(_ error: String)func onVideoDidStartPlayingWith(_ duration: TimeInterval)func onVideoDidStopPlaying()func onVideoDidPlayToEnd()func onVideoPlayingFor(_ seconds: TimeInterval)}
Når visningen modtager AVPlayerLayer fra præsentatoren, kan den genindlæse sin grænseflade.
protocol VPKVideoViewProtocol: class {func reloadInterface(with playerLayer: AVPlayerLayer)}
INTERAKTØR OG PRESENTER FORHOLD
Forholdet mellem delene i VIPER er anderledes end i andre iOS-mønstre. Hvert objekt holder fast i hinanden (svagt naturligvis). Man kan se det som om, at den ene er delegat for den anden. Se eksemplet nedenfor:
-
Presenter har en svag reference til interactoren. Presentereren bekymrer sig KUN om interaktørens inputprotokol. Videresendelsen af input “didTapPlay”.
protocol VPKVideoPlaybackPresenterProtocol: class {var interactor: VPKVideoPlaybackInteractorInputProtocol? { get set }//svag}
-
Presenter videresender inputmeddelelsen; men Presenter er også afhængig af interactors viden om videoenhedsdataene.
public class VPKVideoPlaybackPresenter:VPKVideoPlaybackPresenterProtocol {func didTapVideoView() {guard let safeVideoUrl = interactor?.videoType.videoUrl else {return }interactor?.didTapVideo(videoURL: safeVideoUrl, at:self.indexPath)}}}
PROTOCOL CONFORMANCE
Relationen mellem præsentator og interaktør er den vigtigste del af datastrømmen.
Præsentanten bekymrer sig om interaktørens output
Interaktøren bekymrer sig om præsentantens input
Se nedenfor:
Præsentatoren overholder interaktørens outputprotokol.
//MARK: INTERACTOR --> Presenterextension VPKVideoPlaybackPresenter:VPKVideoPlaybackInteractorOutputProtocol { func onVideoLoadSuccess(_ playerLayer: AVPlayerLayer) {videoView?.reloadInterface(with: playerLayer)
}}}
Interaktøren bruger den svage egenskab af præsentatoren til at sende outputdataene tilbage.
public class VPKVideoPlaybackInteractor:VPKVideoPlaybackInteractorProtocol {
weak var presenter: VPKVideoPlaybackInteractorOutputProtocol?
func playbackManager(_: VPKVideoPlaybackManager, didPrepareplayerLayer: AVPlayerLayer) {presenter?.onVideoLoadSuccess(playerLayer)
}}}
DET PROS
KONSEKVENSER
ANBEFALING
Vi anbefaler at bruge en form for VIPER, hvis:
- Dit team er åbent for at lære et nyt mønster
- Dit team har mindst 1 uge til at lære, forstå og eksperimentere med det
- Det er nemmere at understøtte en testdrevet tilgang på grund af mønsterets afkoblede natur
- Du er begejstret for det 🙂
- Teammedlemmer er ansvarlige for forskellige moduler
Vi anbefaler ikke at bruge en form for VIPER, hvis:
- Dit team er reaktivt, og funktioner ændres næsten dagligt
- Du bygger en lille applikation, der er et eksperiment og bruges til at teste et problem
- Du er ligeglad med tests
VIPER vil brillere med langsigtede projekter, applikationer hvor sikkerheden og forretningslogikken er høj, og hvor test skal implementeres.
Du vil bemærke, at din kode bliver renere. Du vil nemt kunne adskille funktioner & moduler blandt teammedlemmer. Når der er en fejl i formateringen af brugergrænsefladen, vil du straks vide, at du skal tjekke Presenter-klassen. Når der er en logisk fejl, ved du, at den stammer fra interactoren. Mere klarhed, mindre gift.
ADDITIONAL INFO
Dette emne blev præsenteret på try! Swift NYC 2017. Præsentationen kan findes her.
Alle yderligere try! Swift-foredrag kan findes her.