Puneți controlorii de vedere pe o dietă cu MVVM

În postul meu anterior din această serie, am scris despre modelul Model-View-Controller și unele dintre imperfecțiunile sale. În ciuda avantajelor clare pe care le aduce MVC dezvoltării software-ului, acesta tinde să nu se încadreze în aplicațiile mari sau complexe de cacao.

Totuși, aceasta nu este o știre. Mai multe modele arhitecturale au apărut de-a lungul anilor, urmărind să abordeze deficiențele modelului Model-View-Controller. S-ar putea să fi auzit de MVP, Model-View-Presenter și MVVM, Model-View-ViewModel, de exemplu. Aceste modele arată și se simt asemănătoare modelului Model-View-Controller, dar rezolvă și unele dintre problemele pe care modelul Model-View-Controller le suferă.

1. De ce Model-View-ViewModel

Am folosit modelul Model-View-Controller de ani de zile înainte de a cădea accidental asupra lui Model-View-ViewModel model. Nu este surprinzător faptul că MVVM este o întârziere a comunității de cacao, deoarece originea sa duce înapoi la Microsoft. Cu toate acestea, modelul MVVM a fost portat la Cacao și adaptat la cerințele și nevoile cadrelor de cacao și a câștigat recent tendința în comunitatea de cacao.

Cel mai atrăgător este modul în care MVVM se simte ca o versiune îmbunătățită a modelului Model-View-Controller. Aceasta înseamnă că nu necesită o schimbare dramatică a mentalității. De fapt, odată ce înțelegeți fundamentele modelului, este destul de ușor de implementat, nu mai dificil decât implementarea modelului Model-View-Controller.

2. Punerea controlorilor de vedere pe o dietă

În postul anterior, am scris că controlorii dintr-o aplicație tipică de cacao sunt puțin diferiți de controlorii Reenskaug definiți în modelul original MVC. Pe iOS, de exemplu, un controler de vizualizare controlează o vizualizare. Singura sa responsabilitate este popularea vederii pe care o administrează și răspunde interacțiunii cu utilizatorul. Dar aceasta nu este singura responsabilitate a controlorilor de vizualizare în cele mai multe aplicații iOS, este?

Modelul MVVM introduce oa patra componentă a amestecului model de vizualizare, care ajută la reorientarea controlerului de vizualizare. Aceasta face acest lucru preluând unele dintre responsabilitățile controlorului vizual. Consultați schema de mai jos pentru a înțelege mai bine modul în care modelul de vizualizare se potrivește modelului Model-View-ViewModel.

După cum ilustrează diagrama, controlerul de vizualizare nu mai deține modelul. Este modelul de vizualizare care deține modelul, iar controlerul de vizualizare cere modelul de vizualizare pentru datele pe care trebuie să le afișeze.

Aceasta este o diferență importantă de modelul Model-View-Controller. Controlerul de vizualizare nu are acces direct la model. Modelul de vizualizare împarte controlorului de vizualizare datele pe care trebuie să le afișeze în vizualizarea sa.

Relația dintre controlerul de vizualizare și punctul său de vedere rămâne neschimbată. Acest lucru este important, deoarece înseamnă că controlerul de vizualizare se poate concentra exclusiv pe popularea vederii și manipularea interacțiunii utilizatorilor. Pentru asta a fost proiectat controllerul de vizualizare.

Rezultatul este destul de dramatic. Controlerul de vizualizare este pus pe o dietă și multe responsabilități sunt transferate la modelul de vizualizare. Nu mai aveți sfârșit cu un controler de vizualizare care cuprinde sute sau chiar mii de linii de cod.

3. Responsabilitățile modelului de vizualizare

Probabil vă întrebați cum se potrivește modelul de vizualizare cu imaginea mai mare. Care sunt sarcinile modelului de vizualizare? Cum se raportează la controlerul de vizualizare? Și modelul?

Diagrama pe care ți-am arătat-o ​​mai devreme ne oferă câteva sugestii. Să începem cu modelul. Modelul nu mai este deținut de controlorul de vizualizare. Modelul de vizualizare deține modelul și acționează ca proxy pentru controlerul de vizualizare. Ori de câte ori controlerul de vizualizare are nevoie de o piesă de date din modelul său de vizualizare, acesta din urmă îi întreabă modelul pentru datele brute și îl formatează astfel încât controlerul de vizualizare să îl poată folosi imediat în viziunea sa. Controlerul de vizualizare nu este responsabil pentru manipularea și formatarea datelor.

Diagrama arată, de asemenea, că modelul este proprietatea modelului de vizualizare, nu a regulatorului de vizualizare. De asemenea, merită să subliniem că modelul Model-View-ViewModel respectă relația strânsă dintre controlerul de vizualizare și viziunea acestuia, caracteristic aplicațiilor de cacao. De aceea MVVM se simte ca o potrivire naturală pentru aplicațiile de cacao.

4. Un exemplu

Deoarece modelul Model-View-ViewModel nu este originar din cacao, nu există reguli stricte pentru implementarea modelului. Din păcate, acest lucru este ceva ce mulți dezvoltatori se confundă. Pentru a clarifica câteva lucruri, aș dori să vă arăt un exemplu de bază pentru o aplicație care utilizează modelul MVVM. Creăm o aplicație foarte simplă, care preia datele meteo pentru o locație predefinită din API-ul Dark Sky și afișează temperatura curentă utilizatorului.

Pasul 1: Configurați proiectul

Activați Xcode și creați un nou proiect bazat pe Vizualizare individuală șablon. Folosesc Xcode 8 și Swift 3 pentru acest tutorial.

Denumiți proiectul MVVM, și stabilit Limba la Rapid și Dispozitive la iPhone.

Pasul 2: Creați un model de vizualizare

Într-o aplicație tipică de cacao alimentată de modelul Model-View-Controller, controlerul de vizualizare ar fi responsabil de efectuarea cererii de rețea. Aveți posibilitatea să utilizați un manager pentru a efectua solicitarea de rețea, dar controlerul de vizualizare ar ști încă despre originile datelor meteorologice. Mai important, ar primi datele brute și ar trebui să o formateze înainte de a le afișa utilizatorului. Aceasta nu este abordarea pe care o luam la adoptarea modelului Model-View-ViewModel.

Să creăm un model de vizualizare. Creați un nou fișier Swift, denumiți-l WeatherViewViewModel.swift, și să definească o clasă numită WeatherViewViewModel.

import Clasa de fundație WeatherViewViewModel 

Ideea este simplă. Controlerul de vizualizare solicită modelul de vizualizare pentru temperatura curentă pentru o locație predefinită. Deoarece modelul de vizualizare trimite o solicitare de rețea către Dark Sky API, metoda acceptă o închidere, care este invocată atunci când modelul de vizualizare are date pentru controlerul de vizualizare. Aceste date ar putea fi temperatura curentă, dar ar putea fi și un mesaj de eroare. Aceasta este ceea ce currentTemperature (finalizare :) metoda modelului de vizualizare arată. Vom completa detaliile în câteva momente.

import Clasa de fundație WeatherViewViewModel // MARK: - Tip Alias ​​typealias CurrentTemperatureCompletion = (String) -> Void // MARK: - API public funcționalTemperature (completare: @escaping CurrentTemperatureCompletion) 

Declarăm un alias tip pentru confort și definim o metodă, currentTemperature (finalizare :), care acceptă o închidere de tip CurrentTemperatureCompletion

Implementarea nu este greu dacă sunteți familiarizat cu crearea de rețele și cu URLSession API-ul. Uitați-vă la codul de mai jos și observați că am folosit un enum, API-ul, pentru a păstra totul frumos și ordonat.

import Clasa de fundație WeatherViewViewModel // MARK: - Tip Alias ​​typealias CurrentTemperatureCompletion = (String) -> Void // MARK: - API enum API static let lat = 37.8267 static lasat lung = -122.4233 static let APIKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" static let baseURL = URL (șir: "https://api.darksky.net/forecast")! static var requestURL: URL întoarcere API.baseURL.appendingPathComponent (API.APIKey) .appendingPathComponent ("\ (lat), \ (long)") // MARK: - API public func currentTemperature (completare: @escaping CurrentTemperatureCompletion) let dataTask = URLSession.shared.dataTask (cu: API.requestURL) [self self] (date, răspuns, eroare) în // Helpers var formattedTemperature: String? dacă da date = date formattedTemperature = self? .temperature (de la: data) DispatchQueue.main.async completare (formattedTemperature? 

Singura bucată de cod pe care nu ți-am arătat-o ​​încă este implementarea Temperatura (de la :) metodă. În această metodă, extragem temperatura curentă din răspunsul Dark Sky.

// MARK: - Metode helper func temperatura (din date: Data) -> String? paznic lăsați JSON = încercați? JSONSerialization.jsonObject (cu: date, opțiuni: []) ca? [String: Oricare] altcineva return nil paza lăsați în prezent = JSON? ["În prezent"] ca? [String: Oricare] altceva return nil paza permite temperatura = momentan ["temperature"] ca? Dublu altceva return zero retur String (format: "% .0f ° F", temperature)

Într-o aplicație de producție, aș opta pentru o soluție mai robustă pentru a analiza răspunsul, cum ar fi ObjectMapper sau Unbox.

Pasul 3: Integrați modelul de vizualizare

Acum putem folosi modelul de vizualizare în controlerul de vizualizare. Creăm o proprietate pentru modelul de vizualizare și de asemenea definim trei puncte de desfacere pentru interfața cu utilizatorul.

import UIKit clasa ViewController: UIViewController // MARK: - Proprietăți @ IBOutlet var temperatureLabel: UILabel! // MARK: - @IBOutlet var fetchWeatherDataButton: UIButton! // MARK: - @IBOutlet var activitateIndicatorVizualizare: UIActivityIndicatorVizualizare! // MARK: - privat permite vizualizareaModel = WeatherViewViewModel ()

Observați că controlerul de vizualizare deține modelul de vizualizare. În acest exemplu, controlerul de vizualizare este, de asemenea, responsabil pentru instanțializarea modelului său de vizualizare. În general, prefer să injectez modelul de vizualizare în controlerul de vizualizare, dar să o păstrăm simplu pentru moment.

În controlerul de vizualizare viewDidLoad () , invocăm o metodă de ajutor, fetchWeatherData ().

// MARK: - Vizualizați suprascrierea ciclului de viață func viewDidLoad () super.viewDidLoad () // Fetch Data meteo fetchWeatherData ()

În fetchWeatherData (), cerem modelul de vizualizare pentru temperatura curentă. Înainte de a cere temperatura, vom ascunde eticheta și butonul și vom afișa vizualizarea indicatorului de activitate. În închidere trecem la fetchWeatherData (finalizare :), actualizăm interfața de utilizator populând eticheta de temperatură și ascundem afișarea indicatorului de activitate.

// MARK: - Metode Helper private func fetchWeatherData () // Ascunde interfața cu utilizatorul temperatureLabel.isHidden = true fetchWeatherDataButton.isHidden = true // Afișează Indicatorul de activitate Vezi activityIndicatorView.startAnimating () // Fetch Weather Data ViewModel.currentTemperature [unowned auto] (temperatura) în // Actualizați eticheta de temperatură self.temperatureLabel.text = temperatură auto.temperaturăLabel.isHidden = false // Afișați butonul de preluare a datelor meteo de sosire self.fetchWeatherDataButton.isHidden = false // Ascundeți indicatorul de activitate Vizualizați autoactivitateaIndicatorView.stopAnimating ()

Butonul este conectat la o acțiune, fetchWeatherData (_ :), în care invocăm de asemenea fetchWeatherData () metoda helper. După cum puteți vedea, metoda helper ne ajută să evităm duplicarea codului.

// MARK: - Acțiuni @IBAction func fetchWeatherData (_ expeditor: Oricare) // Fetch Data meteo fetchWeatherData ()

Pasul 4: Creați interfața de utilizator

Ultima piesă a puzzle-ului este crearea interfeței de utilizator a aplicației de exemplu. Deschis Main.storyboard și adăugați o etichetă și un buton la o vizualizare verticală a stivei. De asemenea, vom adăuga o afișare a indicatorului de activitate în partea superioară a vederii stack, centrat vertical și orizontal.

Nu uitați să conectați prizele și acțiunea pe care am definit-o în ViewController clasă!

Acum, construiți și rulați aplicația pentru ao încerca. Rețineți că aveți nevoie de o cheie API Dark Sky pentru ca aplicația să funcționeze. Vă puteți înscrie pentru un cont gratuit pe site-ul Dark Sky.

5. Care sunt beneficiile?

Chiar dacă am mutat doar câteva bucăți la modelul de vizualizare, poate vă întrebați de ce este necesar acest lucru. Ce am câștigat? De ce ați adăuga acest nivel suplimentar de complexitate?

Cel mai evident câștig este acela că controlerul de vizualizare este mai slab și mai concentrat pe gestionarea viziunii sale. Aceasta este sarcina de bază a unui controlor de vedere: gestionarea viziunii sale.

Dar există un beneficiu mai subtil. Deoarece controlerul de vizualizare nu este responsabil pentru preluarea datelor meteorologice din API-ul Dark Sky, nu este conștient de detaliile legate de această sarcină. Datele meteorologice pot proveni de la un alt serviciu meteorologic sau de la un răspuns din cache. Operatorul de vizualizare nu ar ști, și nu trebuie să știe.

De asemenea, testarea se îmbunătățește dramatic. Vizualizarea controlorilor este cunoscută a fi dificil de testat din cauza relației lor strânse cu stratul de vizualizare. Prin mutarea unei anumite logici de afaceri în modelul de vizualizare, îmbunătățim instantaneu testabilitatea proiectului. Testarea modelelor de vizualizare este surprinzător de simplă, deoarece acestea nu au o legătură cu stratul vizual al aplicației.

Concluzie

Modelul Model-View-ViewModel reprezintă un pas important în proiectarea aplicațiilor de cacao. Controllerele de vizualizare nu sunt atât de masive, modelele de vizualizare sunt mai ușor de compilat și testate, iar proiectul dvs. devine mai ușor de gestionat ca rezultat.

În această serie scurtă, am zgâriat doar suprafața. Mai aveți multe de scris despre modelul Model-View-ViewModel. A devenit unul dintre modelele mele preferate de-a lungul anilor, și de aceea am continuat să vorbesc și să scriu despre asta. Dă-o o încercare și spune-mi ce crezi!

Între timp, verificați câteva dintre celelalte postări despre dezvoltarea aplicațiilor Swift și iOS.

Cod