Metti i controller della tua vista a dieta con MVVM

Nel mio post precedente in questa serie, ho scritto sul modello Model-View-Controller e alcune delle sue imperfezioni. Nonostante gli evidenti vantaggi che MVC apporta allo sviluppo del software, tende a non essere all'altezza in applicazioni Cocoa di grandi dimensioni o complesse.

Questa non è una novità, però. Diversi modelli architettonici sono emersi nel corso degli anni, con lo scopo di affrontare le carenze del modello Model-View-Controller. Potresti aver sentito parlare MVP, Model-View-Presenter e MVVM, Model-View-ViewModel, ad esempio. Questi pattern hanno un aspetto simile al modello Model-View-Controller, ma affrontano anche alcuni dei problemi che il modello Model-View-Controller soffre di.

1. Perché Model-View-ViewModel

Ho usato il modello Model-View-Controller per anni prima che mi imbattessi casualmente in Model-View-ViewModel modello. Non è sorprendente che MVVM sia un ritardatario della comunità di Cocoa, poiché le sue origini riconducono a Microsoft. Tuttavia, il pattern MVVM è stato convertito in Cocoa e adattato ai requisiti e alle esigenze delle strutture Cocoa e ha recentemente guadagnato terreno nella comunità di Cocoa.

La cosa più interessante è come MVVM si sente come una versione migliorata del modello Model-View-Controller. Ciò significa che non richiede un cambiamento radicale di mentalità. Di fatto, una volta compresi i fondamenti del modello, è abbastanza facile da implementare, non più difficile dell'implementazione del modello Model-View-Controller.

2. Mettere i controller di visualizzazione su una dieta

Nel post precedente, ho scritto che i controller in una tipica applicazione Cocoa sono leggermente diversi dai controller Reenskaug definiti nel pattern MVC originale. Su iOS, ad esempio, un controller di visualizzazione controlla una vista. La sua unica responsabilità è di compilare la vista che gestisce e di rispondere all'interazione dell'utente. Ma non è l'unica responsabilità dei controller di vista nella maggior parte delle applicazioni iOS, vero??

Il pattern MVVM introduce un quarto componente nel mix, il guarda il modello, che aiuta a rimettere a fuoco il controller della vista. Lo fa assumendo alcune delle responsabilità del controller di visualizzazione. Dai uno sguardo allo schema qui sotto per capire meglio come il modello di vista si adatta al modello Model-View-ViewModel.

Come mostra il diagramma, il controller della vista non possiede più il modello. È il modello di visualizzazione proprietario del modello e il controller di visualizzazione chiede al modello di visualizzazione i dati che deve essere visualizzato.

Questa è una differenza importante dal modello Model-View-Controller. Il controller della vista non ha accesso diretto al modello. Il modello di vista fornisce al controllore della vista i dati che deve essere visualizzato nella sua vista.

La relazione tra il controller della vista e la sua vista rimane invariata. Questo è importante perché significa che il controller di visualizzazione può concentrarsi esclusivamente sul popolamento della visualizzazione e sulla gestione dell'interazione dell'utente. Questo è ciò per cui è stato progettato il controller di visualizzazione.

Il risultato è piuttosto drammatico. Il controller di visualizzazione viene messo a dieta e molte responsabilità vengono spostate sul modello di visualizzazione. Non si finisce più con un controller di visualizzazione che si estende su centinaia o addirittura migliaia di righe di codice.

3. Responsabilità del modello di vista

Probabilmente ti starai chiedendo in che modo il modello di visualizzazione si inserisce nel quadro generale. Quali sono le attività del modello di visualizzazione? Come si relaziona al controller di visualizzazione? E per quanto riguarda la modella?

Lo schema che ti ho mostrato prima ci dà alcuni suggerimenti. Iniziamo con il modello. Il modello non è più di proprietà del controller della vista. Il modello di vista è proprietario del modello e funge da proxy per il controller di visualizzazione. Ogni volta che il controller della vista ha bisogno di un pezzo di dati dal suo modello di vista, quest'ultimo chiede al modello per i dati grezzi e lo formatta in modo tale che il controller della vista possa immediatamente utilizzarlo nella sua vista. Il controller di visualizzazione non è responsabile della manipolazione e della formattazione dei dati.

Il diagramma rivela anche che il modello è di proprietà del modello di vista, non del controller di visualizzazione. Vale anche la pena sottolineare che il modello Model-View-ViewModel rispetta la stretta relazione tra il controller della vista e la sua vista, caratteristica tipica delle applicazioni Cocoa. Ecco perché MVVM si presenta come una scelta naturale per le applicazioni Cocoa.

4. Un esempio

Poiché il pattern Model-View-ViewModel non è nativo di Cocoa, non esistono regole rigide per implementare il pattern. Sfortunatamente, questo è qualcosa di cui molti sviluppatori si confondono. Per chiarire alcune cose, mi piacerebbe mostrarvi un esempio di base di un'applicazione che utilizza il pattern MVVM. Creiamo un'applicazione molto semplice che recupera i dati meteorologici per una posizione predefinita dall'API di Dark Sky e visualizza la temperatura corrente all'utente.

Passaggio 1: impostare il progetto

Accendi Xcode e crea un nuovo progetto basato su Applicazione vista singola modello. Sto usando Xcode 8 e Swift 3 per questo tutorial.

Assegna un nome al progetto MVVM, e impostare linguaggio a veloce e dispositivi a i phone.

Passaggio 2: creare un modello di vista

In una tipica applicazione Cocoa alimentata dal pattern Model-View-Controller, il controller di visualizzazione sarebbe incaricato di eseguire la richiesta di rete. È possibile utilizzare un gestore per eseguire la richiesta di rete, ma il controller di visualizzazione sarebbe ancora a conoscenza delle origini dei dati meteorologici. Ancora più importante, riceverebbe i dati grezzi e avrebbe bisogno di formattarli prima di visualizzarli all'utente. Questo non è l'approccio che adottiamo quando adottiamo il modello Model-View-ViewModel.

Creiamo un modello di vista. Crea un nuovo file Swift, chiamalo WeatherViewViewModel.swift, e definire una classe chiamata WeatherViewViewModel.

importare la classe Foundation WeatherViewViewModel 

L'idea è semplice. Il controller della vista chiede al modello di visualizzazione la temperatura corrente per una posizione predefinita. Poiché il modello di visualizzazione invia una richiesta di rete all'API di Dark Sky, il metodo accetta una chiusura, che viene richiamata quando il modello di visualizzazione contiene dati per il controller di visualizzazione. Questi dati potrebbero essere la temperatura corrente, ma potrebbe anche essere un messaggio di errore. Questo è ciò che currentTemperature (completamento :) il metodo del modello di vista è simile. Inseriremo i dettagli in pochi istanti.

import Foundation class WeatherViewViewModel // MARK: - Tipo Alias ​​typealias CurrentTemperatureCompletion = (String) -> Void // MARK: - Funzione API pubblica current currentTemperature (completamento: @escaping CurrentTemperatureCompletion) 

Dichiariamo un alias di tipo per comodità e definiamo un metodo, currentTemperature (completamento :), che accetta una chiusura di tipo CurrentTemperatureCompletion

L'implementazione non è difficile se si ha familiarità con il networking e il URLSession API. Dai un'occhiata al codice qui sotto e nota che ho usato un enum, API, per mantenere tutto bello e in ordine.

import Foundation class WeatherViewViewModel // MARK: - Tipo Alias ​​typealias CurrentTemperatureCompletion = (String) -> Void // MARK: - API enum API static let lat = 37.8267 static let long = -122.4233 static let APIKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" static let baseURL = URL (stringa: "https://api.darksky.net/forecast")! static var requestURL: URL return API.baseURL .appendingPathComponent (API.APIKey) .appendingPathComponent ("\ (lat), \ (long)") // MARK: - Funzione API pubblica currentTemperature (completamento: @escaping CurrentTemperatureCompletion) let dataTask = URLSession.shared.dataTask (con: API.requestURL) [self debole] (dati, risposta, errore) in // Helpers var formattedTemperature: String? se let data = data formattedTemperature = self? .temperature (from: data) DispatchQueue.main.async completion (formattedTemperature ?? "Impossibile recuperare i dati meteo") // Riprendi Data Task dataTask.resume () 

L'unico pezzo di codice che non ti ho ancora mostrato è l'implementazione di temperatura (da :) metodo. In questo metodo, estraiamo la temperatura corrente dalla risposta di Dark Sky.

// MARCHIO: - Metodi di supporto funzione temperatura (da dati: dati) -> stringa? la guardia lascia JSON = provare? JSONSerialization.jsonObject (con: data, options: []) come? [String: Any] else return nil guard lascia attualmente = JSON? ["Attualmente"] come? [String: Any] else return nil guard let temperature = current ["temperature"] come? Double else return nil return String (formato: "% .0f ° F", temperatura)

In un'applicazione di produzione, opterei per una soluzione più robusta per analizzare la risposta, come ObjectMapper o Unbox.

Passaggio 3: integrazione del modello di vista

Ora possiamo usare il modello di vista nel controller della vista. Creiamo una proprietà per il modello di vista e definiamo anche tre punti vendita per l'interfaccia utente.

import classe UIKit ViewController: UIViewController // MARK: - Proprietà @IBOutlet var temperatureLabel: UILabel! // MARK: - @IBOutlet var fetchWeatherDataButton: UIButton! // MARK: - @IBOutlet var activityIndicatorView: UIActivityIndicatorView! // MARK: - private let viewModel = WeatherViewViewModel ()

Si noti che il controller della vista possiede il modello di vista. In questo esempio, il controller della vista è anche responsabile per l'istanziazione del suo modello di vista. In generale, preferisco iniettare il modello di vista nel controller della vista, ma per ora è semplice.

Nel controller della vista viewDidLoad () metodo, invochiamo un metodo di supporto, fetchWeatherData ().

// MARK: - Visualizza Life Cycle Override func viewDidLoad () super.viewDidLoad () // Recupera Dati meteo fetchWeatherData ()

Nel fetchWeatherData (), chiediamo il modello di visualizzazione per la temperatura corrente. Prima di richiedere la temperatura, nasconderemo l'etichetta e il pulsante e mostreremo la visualizzazione dell'indicatore di attività. Nella chiusura passiamo a fetchWeatherData (completamento :), aggiorniamo l'interfaccia utente inserendo l'etichetta della temperatura e nascondendo la visualizzazione dell'indicatore di attività.

// MARK: - Metodi helper private func fetchWeatherData () // Nascondi interfaccia utente temperatureLabel.isHidden = true fetchWeatherDataButton.isHidden = true // Mostra indicatore attività Visualizza activityIndicatorView.startAnimating () // Recupera dati meteo ViewModel.currentTemperature [unowned self] (temperatura) in // Etichetta temperatura di aggiornamento self.temperatureLabel.text = temperatura self.temperatureLabel.isHidden = false // Mostra il pulsante Fetch Weather Data self.fetchWeatherDataButton.isHidden = false // Nascondi Activity Indicator Visualizza self.activityIndicatorView.stopAnimating ()

Il pulsante è collegato a un'azione, fetchWeatherData (_ :), in cui invochiamo anche il fetchWeatherData () metodo di supporto. Come puoi vedere, il metodo helper ci aiuta a evitare la duplicazione del codice.

// MARK: - Azioni @IBAction func fetchWeatherData (_ sender: Any) // Recupera dati meteo fetchWeatherData ()

Passaggio 4: creare l'interfaccia utente

L'ultimo pezzo del puzzle è creare l'interfaccia utente dell'applicazione di esempio. Aperto Main.storyboard e aggiungi un'etichetta e un pulsante a una vista verticale. Aggiungiamo anche una vista di indicatore di attività in cima alla vista dello stack, centrata verticalmente e orizzontalmente.

Non dimenticare di cablare le prese e l'azione che abbiamo definito nel ViewController classe!

Ora crea ed esegui l'applicazione per fare un tentativo. Ricorda che hai bisogno di una chiave API Dark Sky per far funzionare l'applicazione. Puoi registrarti per un account gratuito sul sito web di Dark Sky.

5. Quali sono i vantaggi?

Anche se abbiamo solo spostato alcuni frammenti nel modello di visualizzazione, ci si potrebbe chiedere perché sia ​​necessario. Cosa abbiamo guadagnato? Perché dovresti aggiungere questo ulteriore livello di complessità?

Il guadagno più ovvio è che il controller di visualizzazione è più snello e più concentrato sulla gestione della vista. Questo è il compito principale di un controller di visualizzazione: gestire la sua vista.

Ma c'è un vantaggio più sottile. Poiché il controller di visualizzazione non è responsabile del recupero dei dati meteo dall'API di Dark Sky, non è a conoscenza dei dettagli relativi a questa attività. I dati meteorologici potrebbero provenire da un servizio meteorologico diverso o da una risposta memorizzata nella cache. Il controller della vista non lo saprebbe e non ha bisogno di sapere.

Anche i test migliorano notevolmente. I controller di vista sono noti per essere difficili da testare a causa della loro stretta relazione con il livello di vista. Spostando alcune delle logiche di business nel modello di visualizzazione, miglioriamo istantaneamente la testabilità del progetto. Testare i modelli di visualizzazione è sorprendentemente facile perché non hanno un collegamento al livello di vista dell'applicazione.

Conclusione

Il modello Model-View-ViewModel rappresenta un significativo passo avanti nella progettazione delle applicazioni Cocoa. I controller di vista non sono così grandi, i modelli di vista sono più facili da comporre e testare e il tuo progetto diventa più gestibile di conseguenza.

In questa breve serie, abbiamo solo graffiato la superficie. C'è molto di più da scrivere sul modello Model-View-ViewModel. È diventato uno dei miei modelli preferiti nel corso degli anni, ed è per questo che continuo a parlare e scrivere di questo. Fai un tentativo e fammi sapere cosa ne pensi!

Nel frattempo, dai uno sguardo ad alcuni dei nostri altri post sullo sviluppo di app Swift e iOS.