VIPER är komplext, skrämmande och annorlunda. Det finns en antydan till gift under ytan av VIPER och det kan göra ont. Men med tiden avtar smärtan och den blir en av botemedlen för att strukturera din kod. Bye bye venom. Hej lycka.
Här är vad iOS-utvecklare kan relatera till:
Designmönstret VIPER är en källa till förvirring & missförstånd för många utvecklare. Fördelarna & nackdelarna och den praktiska användningen av detta mönster är oklara. Efter att ha arbetat med applikationer med mönstren MVC, MVVM, MVVM, MVVM + RxSwift ville jag få en fördjupad förståelse för detta elakt klingande designmönster och se vad surret (väsandet?) handlade om. Därför bestämde jag mig för att testa följande hypotes:
Som utvecklare skulle jag vilja använda VIPER-designmönstret för att bygga återanvändbara moduler.
Vi skrev det här inlägget i hopp om att det ger dig möjlighet att utvärdera om VIPER är rätt alternativ för ditt projekt eller ramverk.
VIPER
Ett par personer på Mutual Mobile ville förbättra testtäckningen på sina iOS-appar. Som ett resultat kom de på själva mönstret. Mer om var det kom ifrån finns här.
Litterärt sett är Viper en backronym som står för View, Interactor, Presenter, Entity, Router.
Teoretiskt sett skulle din applikation eller ditt ramverk, när du använder Viper, delas upp i VIPER-”moduler” – återanvändbara & modulkomponenter. I diagramform kan det översättas till följande:
View:
Interaktion: Gränssnittsskiktet lägger ut vyn, uppdaterar bilder och etiketter, skickar användarinput till presentatören
Interaktör: Denna funktion är en del av en funktion som är en del av ett gränssnitt: Affärslogik, hantering av data, omvandling av modeller, interagerar med API-tjänster & datahanteringsklasser. Överför utdata till presentatören
Presentatör: Formaterar data som tas emot från interaktören för vyn, tar emot indata från vyn och vidarebefordrar den till interaktören
Entitet: En enkel data som representerar det som behövs för att VIPER-modulen ska fungera
Router: En enkel data som representerar det som behövs för att VIPER-modulen ska fungera
Router: Ansvarar för modulnavigering och programnavigering
VIPER i kod fungerar inte i samma ordning som de bokstäver du läser. Med andra ord… åtgärderna i din kod kommer att flyta mer i nedanstående ordning:
Interaktör → Router , Entity, & Externt → Presenter → View
VIPER blir mer som IREPV… Det är ett ord till någon, någonstans.
EXPERIMENT
Vi validerade hypotesen om att använda VIPER i en återanvändbar modul genom att bygga en ram. Ramverket är inriktat på att spela upp videoinnehåll. Uppspelning av video innebär UI-uppdateringar, datahämtning & datasynkronisering. Dessa interaktioner bevisade att en modul för videouppspelning skulle vara en värdefull kandidat för en strukturerad VIPER-modul.
ÖVERSIKT
VIPER-moduler är protokollorienterade. Varje funktion, egenskap, ingång och utgång implementeras via ett protokoll.
Som ett resultat använde vi idén om en:
Kontrakt: en snabbfil som beskriver alla protokoll som används i VIPER-modulen. Den liknar en objective-C header-fil. Kodutdrag nedan:
//*** 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 &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 ville också att utvecklare av det här ramverket skulle ha ett enkelt sätt att konfigurera VIPER-moduler.
Som ett resultat använde vi idén om en:
Builder: Ett enkelt objekt som används för att konfigurera VIPER-modulen och ställa in beroenden. Det instansierar presentatören och interaktören. Slutligen returnerar den vyn till viewcontrollern.
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 använde också ett protokoll för att ställa in alla beroenden för modulen:
class VPKDependencyManager: VPKDependencyManagerProtocol {
static func videoView(with interactor:VPKVideoPlaybackInteractorProtocol &VKVPKVideoPlaybackInteractorInputProtocol, and presenter:VPKVideoPlaybackPresenterProtocol &VKVPKVideoPlaybackInteractorOutputProtocol) -> 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
Vi kan tänka på användarhistorien:
**PLAY A VIDEO **
Med hjälp av AVFOUNDATION vet vi följande:
- Måste använda AVPlayer
- AVPlayer ger dig en AVPlayerLayer som du kan lägga till i din vy
- AVPlayer tar tid att förbereda – särskilt med fjärrvideo-urls
- Fjärrvideodata behöver tid för att laddas ner
-
Två primära visningsåtgärder
- Avspelning
- Pausera
-
Resultat av dessa åtgärder:
- AVPlayer tar emot video url för att ladda ner data för
- När den är klar returnerar AVPlayer en AVPlayerLayer
- AvPlayerLayer måste läggas till i en vy
- På paus måste AVPlayer stoppa uppspelningen
**IREPV (AKA VIPER) & AVKit, AVFoundation **
INPUTS OCH OUTPUTS
Detta är nyckeln till att förstå mönstret, lugna ner hjärnan och undvika att hamna i komplexa kaninhål.
Stor läxa 1: Förstå ingångarna och utgångarna i din modul.
View
- Input: didTapPlay
- Output: Presenter didTapPlay
Presenter
- Input: didTapPlay tar emot från view
- Output: onPlayerLayerSuccess(playerLayer) – ger ut detta till presentatören
Interager
- Input: DidTapPlay – talar om för den externa förvaltaren
-
Output: onVideoPlayerLayerSuccess – skickar spelarlagret till presentatören
protokoll VPKVideoPlaybackInteractorOutputProtocol: 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 vyn får AVPlayerLayer från presentatören kan den ladda om sitt gränssnitt.
protocol VPKVideoViewProtocol: class {func reloadInterface(with playerLayer: AVPlayerLayer)}
INTERACTOR AND PRESENTER RELATIONSHIP
Relationerna mellan delarna i VIPER skiljer sig från dem i andra iOS-mönster. Varje objekt håller fast vid varandra (svagt förstås). Man kan se det som att det ena är delegat för det andra. Se exemplet nedan:
-
Presenter har en svag referens till interaktören. Presentatören bryr sig ENDAST om interaktörens inmatningsprotokoll. Vidarebefordran av ingången ”didTapPlay”.
protokoll VPKVideoPlaybackPresenterProtocol: class {var interactor: VPKVideoPlaybackInteractorInputProtocol? { get set }//weak}
-
Presenter vidarebefordrar inmatningsmeddelandet, men presenteraren är också beroende av interaktörernas kunskap om videoenhetsdata.
public class VPKVideoPlaybackPresenter:VPKVideoPlaybackPresenterProtocol {func didTapVideoView() {guard let safeVideoUrl = interactor?.videoType.videoUrl else else {return }interactor?.didTapVideo(videoURL: safeVideoUrl, at:self.indexPath)}}
PROTOCOL CONFORMANCE
Relationen mellan presentatör och interaktör är den viktigaste delen av dataflödet.
Presentatorn bryr sig om Interactors Output
Interactor bryr sig om Presenters Input
Se nedan:
Presentatören följer interaktörens utdataprotokoll.
//MARK: INTERACTOR --> Presenterextension VPKVideoPlaybackPresenter:VPKVideoPlaybackInteractorOutputProtocol { func onVideoLoadSuccess(_ playerLayer: AVPlayerLayer) {videoView?.reloadInterface(with: playerLayer)
}}
Interaktören använder presentatörens svaga egenskap för att skicka tillbaka utdata.
public class VPKVideoPlaybackInteractor:VPKVideoPlaybackInteractorProtocol {
weak var presenter: VPKVideoPlaybackInteractorOutputProtocol?
func playbackManager(_: VPKVideoPlaybackManager, didPrepareplayerLayer: AVPlayerLayer) {presenter?.onVideoLoadSuccess(playerLayer)
}}}
DET PROS
Nejdelar
Rekommendation
Vi rekommenderar att man använder en form av VIPER om:
- Ditt team är öppet för att lära sig ett nytt mönster
- Ditt team har minst en vecka på sig att lära sig, förstå och experimentera med det
- Enklare att stödja ett testdrivet tillvägagångssätt på grund av mönstrets frikopplade natur
- Du är entusiastisk över det 🙂
- Teammedlemmar är ansvariga för olika moduler
Vi rekommenderar inte att man använder en form av VIPER om:
- Ditt team är reaktivt och funktionerna ändras nästan dagligen
- Du bygger en liten applikation som är ett experiment och som används för att testa ett problem
- Du bryr dig inte om tester
VIPER kommer att glänsa med långvariga projekt, applikationer där säkerheten och affärslogiken är hög och när testning måste genomföras.
Du kommer att märka att din kod blir renare. Du kommer att kunna separera funktioner &moduler bland teammedlemmarna på ett enkelt sätt. När det finns ett fel i formateringen av användargränssnittet kommer du omedelbart att veta att du ska kontrollera Presenter-klassen. När det finns ett logiskt fel vet du att det kommer från interaktören. Mer klarhet, mindre gift.
UTÖVRIG INFO
Detta ämne presenterades på try! Swift NYC 2017. Presentationen kan hittas här.
Alla ytterligare try! Swift-föreläsningar finns här.