In mijn vorige post in deze serie schreef ik over het patroon Model-View-Controller en enkele onvolkomenheden. Ondanks de duidelijke voordelen die MVC biedt voor de ontwikkeling van software, heeft het de neiging tekort te schieten in grote of complexe Cocoa-toepassingen.
Dit is echter geen nieuws. Verschillende architecturale patronen zijn in de loop van de jaren naar voren gekomen, gericht op het aanpakken van de tekortkomingen van het Model-View-Controller patroon. Je hebt misschien wel gehoord van MVP, Model-View-Presenter, en MVVM, Model-View-ViewModel, bijvoorbeeld. Deze patronen zien eruit en voelen hetzelfde als het patroon Model-View-Controller, maar ze pakken ook enkele van de problemen aan die het Model-View-Controller-patroon ondervindt.
Ik gebruikte het Model-View-Controller-patroon al jaren voordat ik per ongeluk struikelde over de Model-View-ViewModel patroon. Het is niet verwonderlijk dat MVVM een laatkomer is voor de Cocoa-gemeenschap, omdat de oorsprong teruggaat naar Microsoft. Het MVVM-patroon is echter geporteerd naar Cocoa en aangepast aan de vereisten en behoeften van de Cocoa-kaders en heeft recentelijk zijn sporen verdiend in de Cocoa-gemeenschap.
Het aantrekkelijkst is hoe MVVM aanvoelt als een verbeterde versie van het patroon Model-View-Controller. Dit betekent dat het geen dramatische verandering van mindset vereist. Als je eenmaal de grondbeginselen van het patroon begrijpt, is het eigenlijk vrij eenvoudig te implementeren, niet moeilijker dan het implementeren van het Model-View-Controller-patroon.
In de vorige post schreef ik dat de controllers in een typische Cocoa-applicatie een beetje verschillen van de controllers Reenskaug die in het oorspronkelijke MVC-patroon zijn gedefinieerd. Op iOS beheert een weergavecontroller bijvoorbeeld een weergave. De enige verantwoordelijkheid is het invullen van de weergave die het beheert en reageert op gebruikersinteractie. Maar dat is niet de enige verantwoordelijkheid van view controllers in de meeste iOS-applicaties?
Het MVVM-patroon introduceert een vierde component in de mix, de bekijk het model, wat helpt de focuscontroller opnieuw te focussen. Dit gebeurt door een aantal taken van de view controller over te nemen. Bekijk het onderstaande diagram om beter te begrijpen hoe het weergavemodel past in het patroon Model-View-ViewModel.
Zoals het diagram laat zien, is de view controller niet langer eigenaar van het model. Het is het weergavemodel dat het model bezit en de weergavecontroller vraagt het weergavemodel om de gegevens die het moet weergeven.
Dit is een belangrijk verschil met het patroon Model-View-Controller. De view controller heeft geen directe toegang tot het model. Het weergavemodel geeft de view controller de gegevens die het nodig heeft om weer te geven in zijn weergave.
De relatie tussen de view controller en zijn weergave blijft ongewijzigd. Dat is belangrijk omdat het betekent dat de weergavecontroller zich uitsluitend kan richten op het invullen van de weergave en het omgaan met gebruikersinteractie. Dat is waar de view controller voor is ontworpen.
Het resultaat is behoorlijk dramatisch. De view controller wordt op dieet gezet en veel verantwoordelijkheden worden verschoven naar het viewmodel. Je eindigt niet langer met een view controller die honderden of zelfs duizenden regels code beslaat.
Je vraagt je waarschijnlijk af hoe het kijkmodel in het grotere geheel past. Wat zijn de taken van het weergavemodel? Hoe verhoudt dit zich tot de view controller? En hoe zit het met het model?
Het diagram dat ik je eerder heb laten zien geeft ons een paar hints. Laten we beginnen met het model. Het model is niet langer eigendom van de view controller. Het weergavemodel bezit het model en fungeert als een proxy voor de view controller. Wanneer de view controller een stuk data van zijn view-model nodig heeft, vraagt deze zijn model om de onbewerkte data en formatteert deze zodanig dat de view-controller deze onmiddellijk in zijn weergave kan gebruiken. De view controller is niet verantwoordelijk voor gegevensmanipulatie en -formattering.
Het diagram laat ook zien dat het model eigendom is van het weergavemodel, niet van de weergaveregelaar. Het is ook de moeite waard om erop te wijzen dat de patroon Model-View-ViewModel de nauwe relatie van de weergavecontroller en de weergave ervan respecteert, wat kenmerkend is voor Cocoa-toepassingen. Daarom voelt MVVM als een natuurlijke pasvorm voor Cocoa-toepassingen.
Omdat het patroon Model-View-ViewModel niet afkomstig is van Cocoa, zijn er geen strikte regels om het patroon te implementeren. Helaas zijn dit veel ontwikkelaars in de war raken door. Om een paar dingen duidelijk te maken, wil ik u een eenvoudig voorbeeld tonen van een toepassing die het MVVM-patroon gebruikt. We maken een zeer eenvoudige applicatie die weergegevens van een vooraf gedefinieerde locatie uit de Dark Sky API ophaalt en de huidige temperatuur aan de gebruiker toont.
Start Xcode op en maak een nieuw project op basis van de Toepassing enkele weergave sjabloon. Ik gebruik Xcode 8 en Swift 3 voor deze tutorial.
Geef het project een naam MVVM, En instellen Taal naar Snel en apparaten naar iPhone.
In een typische Cocoa-toepassing die wordt aangedreven door het patroon Model-View-Controller, zou de view controller verantwoordelijk zijn voor het uitvoeren van de netwerkaanvraag. U zou een manager kunnen gebruiken voor het uitvoeren van de netwerkaanvraag, maar de view controller zou nog steeds weten over de oorsprong van de weergegevens. Wat nog belangrijker is, zou het de onbewerkte gegevens ontvangen en zou het moeten formatteren alvorens het aan de gebruiker te tonen. Dit is niet de benadering die we volgen bij het adopteren van het patroon Model-View-ViewModel.
Laten we een weergavemodel maken. Maak een nieuw Swift-bestand, noem het WeatherViewViewModel.swift, en definieer een klasse met de naam WeatherViewViewModel
.
import Foundation class WeatherViewViewModel
Het idee is eenvoudig. De weergavecontroller vraagt het weergavemodel om de huidige temperatuur voor een vooraf gedefinieerde locatie. Omdat het weergavemodel een netwerkaanvraag naar de Dark Sky API verzendt, accepteert de methode een afsluiting, die wordt aangeroepen wanneer het weergavemodel gegevens voor de weergaveregelaar bevat. Die gegevens kunnen de huidige temperatuur zijn, maar het kan ook een foutmelding zijn. Dit is wat de currentTemperature (oplevering :)
methode van het uitzichtmodel lijkt. We zullen de details in enkele ogenblikken invullen.
importeren Foundation class WeatherViewViewModel // MARK: - Type Alias typealias CurrentTemperatureCompletion = (String) -> Void // MARK: - Public API func currentTemperature (completion: @escaping CurrentTemperatureCompletion)
We declareren een type alias voor het gemak en definiëren een methode, currentTemperature (oplevering :)
, dat accepteert een sluiting van het type CurrentTemperatureCompletion
.
De implementatie is niet moeilijk als u bekend bent met netwerken en de URLSession
API. Bekijk de onderstaande code en merk op dat ik een enum heb gebruikt, API
, om alles netjes en opgeruimd te houden.
import Foundation class WeatherViewViewModel // MARK: - Type Alias typealias CurrentTemperatureCompletion = (String) -> Void // MARK: - API enum API static lat = 37.8267 static let long = -122.4233 static let APIKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" static let baseURL = URL (string: "https://api.darksky.net/forecast")! static var requestURL: URL return API.baseURL .appendingPathComponent (API.APIKey) .appendingPathComponent ("\ (lat), \ (long)") // MARK: - Public API func currentTemperature (completion: @escaping CurrentTemperatureCompletion) let dataTask = URLSession.shared.dataTask (with: API.requestURL) [weak self] (data, response, error) in // Helpers var formatattedTemperature: String? if let data = data formattedTemperature = self? .temperature (from: data) DispatchQueue.main.async completion (formattedTemperature ?? "Kan geen weergegevens ophalen)) // Data-taakgegevens hervattenTask.resume ()
Het enige stukje code dat ik je nog niet heb laten zien is de implementatie van de temperatuur (van :)
methode. In deze methode halen we de huidige temperatuur uit het Dark Sky-antwoord.
// MARK: - Helpermethoden func temperature (from data: Data) -> String? bewaker laat JSON = proberen? JSONSerialization.jsonObject (met: data, opties: []) als? [String: Any] else back nihil bewaker laat momenteel = JSON? ["Op dit moment"] zoals? [String: Any] else return nil bewaker laat temperatuur = momenteel ["temperatuur"] zoals? Double else return nil return String (formaat: "% .0f ° F", temperatuur)
In een productieapplicatie zou ik kiezen voor een meer robuuste oplossing om de respons te analyseren, zoals ObjectMapper of Unbox.
We kunnen het weergavemodel nu gebruiken in de view-controller. We maken een eigenschap voor het weergavemodel en we definiëren ook drie verkooppunten voor de gebruikersinterface.
import UIKit class ViewController: UIViewController // MARK: - Properties @IBOutlet var temperatureLabel: UILabel! // MARK: - @IBOutlet var fetchWeatherDataButton: UIButton! // MARK: - @IBOutlet var activityIndicatorView: UIActivityIndicatorView! // MARK: - private let viewModel = WeatherViewViewModel ()
Merk op dat de view controller eigenaar is van het viewmodel. In dit voorbeeld is de view controller ook verantwoordelijk voor het instantiëren van zijn weergavemodel. Over het algemeen geef ik er de voorkeur aan om het weergavemodel in de view-controller in te spuiten, maar laten we het eenvoudig houden voor nu.
In de view controller's viewDidLoad ()
methode, roepen we een helper-methode op, fetchWeatherData ()
.
// MARK: - View Life Cycle-override-func viewDidLoad () super.viewDidLoad () // Weergegevens ophalen fetchWeatherData ()
In fetchWeatherData ()
, we vragen het zichtmodel voor de huidige temperatuur. Voordat we de temperatuur opvragen, verbergen we het label en de knop en geven we de weergave van de activiteitenindicator weer. In de afsluiting gaan we naar fetchWeatherData (oplevering :)
, we werken de gebruikersinterface bij door het temperatuurlabel in te vullen en de weergave van de activiteitenindicator te verbergen.
// MARK: - Helper Methods private func fetchWeatherData () // Verberg gebruikersinterface temperatureLabel.isHidden = true fetchWeatherDataButton.isHidden = true // Activiteitsindicator tonen Activiteit bekijkenIndicatorView.startAnimating () // Weergegevens ophalen ViewModel.currentTemperature [unowned zelf] (temperatuur) in // Update Temperatuurlabel self.temperatureLabel.text = temperatuur self.temperatureLabel.isHidden = false // Weergaaveld Weergegeven weergeven Knop self.fetchWeatherDataButton.isHidden = false // Verberg Activiteitsindicator Zicht self.activityIndicatorView.stopAnimating ()
De knop is aangesloten op een actie, fetchWeatherData (_ :)
, waarin we ook het fetchWeatherData ()
helper methode. Zoals u kunt zien, helpt de helpermethode ons om codeduplicatie te voorkomen.
// MARK: - Acties @IBAction func fetchWeatherData (_ sender: Any) // Fetch Weather Data fetchWeatherData ()
Het laatste stukje van de puzzel is om de gebruikersinterface van de voorbeeldtoepassing te maken. Open Main.storyboard en voeg een label en een knop toe aan een verticale stapelweergave. We voegen ook een weergave van de activiteitsindicator toe bovenop de stapelweergave, gecentreerd in de hoogte en horizontaal.
Vergeet niet om de stopcontacten en de actie die we hebben gedefinieerd in de ViewController
klasse!
Bouw de applicatie nu en voer hem uit om hem uit te proberen. Vergeet niet dat je een Dark Sky API-sleutel nodig hebt om de applicatie te laten werken. U kunt zich aanmelden voor een gratis account op de Dark Sky-website.
Hoewel we alleen een paar stukjes en beetjes naar het weergavemodel hebben verplaatst, vraag je je misschien af waarom dit nodig is. Wat hebben we gewonnen? Waarom zou je deze extra laag van complexiteit toevoegen??
De meest voor de hand liggende winst is dat de view controller slanker is en meer gericht is op het beheren van zijn weergave. Dat is de kerntaak van een view controller: het beheren van zijn visie.
Maar er is een meer subtiel voordeel. Omdat de view controller niet verantwoordelijk is voor het ophalen van de weergegevens uit de Dark Sky API, is deze niet op de hoogte van de details met betrekking tot deze taak. De weergegevens kunnen afkomstig zijn van een andere weerservice of een reactie in de cache. De view controller zou het niet weten, en het hoeft niet te weten.
Testen verbetert ook dramatisch. Bekijk controllers staan erom bekend dat ze moeilijk te testen zijn vanwege hun nauwe relatie met de view layer. Door een deel van de bedrijfslogica naar het weergavemodel te verplaatsen, verbeteren we de testbaarheid van het project onmiddellijk. Het testen van weergavemodellen is verrassend eenvoudig omdat ze geen koppeling bevatten naar de weergavelaag van de toepassing.
Het Model-View-ViewModel-patroon is een belangrijke stap voorwaarts in het ontwerpen van Cocoa-toepassingen. View-controllers zijn niet zo enorm, view-modellen zijn eenvoudiger te componeren en te testen, en uw project wordt daardoor beter beheersbaar.
In deze korte reeks hebben we alleen maar de oppervlakte bekrast. Er is nog veel meer te schrijven over het patroon Model-View-ViewModel. Het is in de loop der jaren een van mijn favoriete patronen geworden en daarom blijf ik erover praten en schrijven. Probeer het en laat me weten wat je ervan vindt!
Bekijk in de tussentijd een aantal van onze andere berichten over de ontwikkeling van Swift- en iOS-apps.