Il modo giusto per condividere lo stato tra i controller di Swift View

Cosa starai creando

Alcuni anni fa, quando ero ancora impiegato in una società di consulenza mobile, ho lavorato a un'app per una grande banca d'investimenti. Le grandi aziende, in particolare le banche, di solito hanno processi in atto per garantire che il loro software sia sicuro, robusto e manutenibile.

Parte di questo processo comportava l'invio del codice dell'app che ho scritto a una terza parte per la revisione. Questo non mi ha infastidito, perché pensavo che il mio codice fosse impeccabile e che la società di revisione avrebbe detto la stessa cosa.

Quando la loro risposta è tornata, il verdetto era diverso da quello che pensavo. Anche se hanno detto che la qualità del codice non era male, hanno sottolineato il fatto che il codice era difficile da mantenere e da testare (il test delle unità non era molto popolare nello sviluppo iOS allora).

Ho scartato il loro giudizio, pensando che il mio codice fosse ottimo e non c'era modo di migliorarlo. Devono solo non capirlo!

Ho avuto la tipica arroganza dello sviluppatore: spesso pensiamo che ciò che facciamo è grandioso e altri non lo capiscono. 

Col senno di poi ho sbagliato. Poco dopo ho iniziato a leggere alcune best practice. Da quel momento in poi, i problemi nel mio codice iniziarono a sporgere come un pollice dolente. Mi sono reso conto che, come molti sviluppatori iOS, avevo ceduto alle classiche trappole delle cattive pratiche di codifica.

Ciò che la maggior parte degli sviluppatori iOS si sbaglia

Una delle più diffuse pratiche di sviluppo di iOS si verifica quando si passa lo stato tra i controller di visualizzazione di un'app. Io stesso sono caduto in questa trappola nel passato.

La propagazione dello stato tra i controller di visualizzazione è di vitale importanza in qualsiasi app iOS. Mentre i tuoi utenti navigano attraverso gli schermi della tua app e interagiscono con essa, devi mantenere uno stato globale che tenga traccia di tutte le modifiche apportate dall'utente ai dati.

Ed è qui che la maggior parte degli sviluppatori iOS raggiunge la soluzione ovvia, ma errata: il modello singleton.

Il pattern singleton è molto veloce da implementare, specialmente in Swift, e funziona bene. Devi solo aggiungere una variabile statica ad una classe per mantenere un'istanza condivisa della classe stessa, e hai finito.

class Singleton static let shared = Singleton ()

È quindi facile accedere a questa istanza condivisa da qualsiasi punto del codice:

let singleton = Singleton.shared

Per questo motivo, molti sviluppatori pensano di aver trovato la migliore soluzione al problema della propagazione dello stato. Ma hanno torto.

Il modello singleton è in realtà considerato un anti-pattern. Ci sono state molte discussioni su questo nella comunità di sviluppo. Ad esempio, consulta questa domanda sull'overflow dello stack.

In breve, i singleton creano questi problemi:

  • Introducono molte dipendenze nelle tue classi, rendendo più difficile cambiarle in futuro.
  • Rendono lo stato globale accessibile a qualsiasi parte del tuo codice. Questo può creare interazioni complesse difficili da tracciare e causare molti bug inaspettati.
  • Rendono le tue classi molto difficili da testare, dal momento che non puoi separarle facilmente da un singolo.

A questo punto, alcuni sviluppatori pensano: "Ah, ho una soluzione migliore. Userò il AppDelegate anziché".

Il problema è che il AppDelegate la classe nelle app iOS è accessibile tramite UIApplication istanza condivisa:

let appDelegate = UIApplication.shared.delegate

Ma l'istanza condivisa di UIApplication è di per sé un singleton. Quindi non hai risolto nulla!

La soluzione a questo problema è l'iniezione di dipendenza. Iniezione di dipendenza significa che una classe non recupera o crea le proprie dipendenze, ma le riceve dall'esterno.

Per vedere come utilizzare l'iniezione di dipendenza nelle app iOS e come abilitare la condivisione dello stato, per prima cosa è necessario rivisitare uno dei modelli architettonici fondamentali delle app iOS: il modello Model-View-Controller.

Estensione del pattern MVC

Il pattern MVC, in poche parole, afferma che ci sono tre livelli nell'architettura di un'app iOS:

  • Il livello del modello rappresenta i dati di un'app.
  • Il livello di visualizzazione mostra informazioni sullo schermo e consente l'interazione.
  • Lo strato controller agisce da collante tra gli altri due livelli, spostando i dati tra di loro.

La solita rappresentazione del pattern MVC è qualcosa del genere:

Il problema è che questo diagramma è sbagliato.

Questo "segreto" si nasconde in bella vista in un paio di righe nella documentazione di Apple:

"È possibile unire i ruoli MVC riprodotti da un oggetto, facendo sì che un oggetto, ad esempio, soddisfi sia il controller che i ruoli di visualizzazione, nel qual caso si chiamerebbe controller di visualizzazione. Allo stesso modo, puoi anche avere oggetti controllore del modello. "

Molti sviluppatori pensano che i controller di visualizzazione siano gli unici controller esistenti in un'app iOS. Per questo motivo, un sacco di codice finisce per essere scritto al loro interno per mancanza di un posto migliore. Questo è ciò che porta gli sviluppatori a usare i singleton quando hanno bisogno di propagare lo stato: sembra l'unica soluzione possibile.

Dalle linee sopra citate, è chiaro che possiamo aggiungere una nuova entità alla nostra comprensione del pattern MVC: il controller del modello. I controller di modello gestiscono il modello dell'app, rispettando i ruoli che il modello stesso non dovrebbe soddisfare. Questo è in realtà come dovrebbe apparire lo schema sopra:

L'esempio perfetto di quando un controller di modello è utile è per mantenere lo stato dell'app. Il modello dovrebbe rappresentare solo i dati della tua app. Lo stato dell'app non dovrebbe essere la sua preoccupazione.

Questo mantenimento dello stato di solito finisce all'interno dei controller di visualizzazione, ma ora abbiamo un nuovo e migliore posto per metterlo: un controller di modello. Questo controller modello può quindi essere passato per visualizzare i controller non appena vengono visualizzati sullo schermo tramite l'iniezione delle dipendenze.

Abbiamo risolto l'anti-pattern singleton. Vediamo la nostra soluzione in pratica con un esempio.

Propagating State Across View Controllers Using Dependency Injection

Scriveremo una semplice app per vedere un esempio concreto di come funziona. L'app mostrerà la tua offerta preferita su una schermata e ti consentirà di modificare la citazione su una seconda schermata.

Ciò significa che la nostra app avrà bisogno di due controller di vista, che dovranno condividere lo stato. Dopo aver visto come funziona questa soluzione, puoi estendere il concetto a app di qualsiasi dimensione e complessità.

Per iniziare, abbiamo bisogno di un tipo di modello per rappresentare i dati, che nel nostro caso è una citazione. Questo può essere fatto con una semplice struttura:

struct Quote let text: String let author: String

Il controller del modello

Abbiamo quindi bisogno di creare un controller modello che mantenga lo stato dell'app. Questo controller modello deve essere una classe. Questo perché avremo bisogno di una singola istanza che passeremo a tutti i nostri controller di visualizzazione. I tipi di valore come le strutture vengono copiati quando li passiamo, quindi chiaramente non sono la soluzione giusta.

Tutte le esigenze del nostro controller di modello nel nostro esempio sono proprietà in cui è possibile mantenere la quota corrente. Ma, naturalmente, nelle app più grandi i controller di modello possono essere più complessi di questo:

class ModelController var quote = Quote (testo: "Due cose sono infinite: l'universo e la stupidità umana, e non sono sicuro dell'universo.", autore: "Albert Einstein")

Ho assegnato un valore predefinito al citazione proprietà quindi avremo già qualcosa da visualizzare sullo schermo all'avvio dell'app. Questo non è necessario e potresti dichiarare la proprietà come facoltativa inizializzata a zero, se desideri che la tua app si avvii con uno stato vuoto.

Crea l'interfaccia utente

Abbiamo ora il controller del modello, che conterrà lo stato della nostra app. Successivamente, abbiamo bisogno dei controller di visualizzazione che rappresenteranno gli schermi della nostra app.

Innanzitutto, creiamo le loro interfacce utente. Ecco come i due controller di vista guardano all'interno dello storyboard dell'app.

L'interfaccia del primo controller di visualizzazione è composta da un paio di etichette e un pulsante, assemblati con semplici vincoli di layout automatico. (Puoi leggere di più sul layout automatico qui su Envato Tuts +.)

L'interfaccia del secondo controller di visualizzazione è la stessa, ma ha una visualizzazione di testo per modificare il testo della citazione e un campo di testo per modificare l'autore.

I due controller di vista sono collegati da una singola presentazione modale successiva, che ha origine dal Modifica preventivo pulsante.

È possibile esplorare l'interfaccia e i vincoli dei controller della vista nel repository GitHub.

Codifica un controller di vista con iniezione di dipendenza

Ora dobbiamo codificare i nostri controller di visualizzazione. La cosa importante da tenere a mente qui è che hanno bisogno di ricevere l'istanza del controller del modello dall'esterno, attraverso l'iniezione delle dipendenze. Quindi hanno bisogno di esporre una proprietà per questo scopo.

var modelController: ModelController!

Possiamo chiamare il nostro primo controller di visualizzazione QuoteViewController. Questo controller vista ha bisogno di un paio di prese per le etichette per la citazione e l'autore nella sua interfaccia.

class QuoteViewController: UIViewController @IBOutlet weak var quoteTextLabel: UILabel! @IBOutlet weak var quoteAuthorLabel: UILabel! var modelController: ModelController! 

Quando questo controller di visualizzazione viene visualizzato, popoliamo la sua interfaccia per mostrare la citazione corrente. Inseriamo il codice per farlo nel controller viewWillAppear (_ :) metodo.

class QuoteViewController: UIViewController @IBOutlet weak var quoteTextLabel: UILabel! @IBOutlet weak var quoteAuthorLabel: UILabel! var modelController: ModelController! override func viewWillAppear (_ animato: Bool) super.viewWillAppear (animato) lascia quote = modelController.quote quoteTextLabel.text = quote.text quoteAuthorLabel.text = quote.author

Avremmo potuto inserire questo codice nel viewDidLoad () metodo invece, che è abbastanza comune. Il problema, però, è quello viewDidLoad () viene chiamato una sola volta, quando viene creato il controller di visualizzazione. Nella nostra app, abbiamo bisogno di aggiornare l'interfaccia utente di QuoteViewController ogni volta che arriva sullo schermo. Questo perché l'utente può modificare l'offerta sul secondo schermo. 

Questo è il motivo per cui usiamo il viewWillAppear (_ :) metodo invece di viewDidLoad (). In questo modo possiamo aggiornare l'interfaccia utente del controller di visualizzazione ogni volta che appare sullo schermo. Se vuoi saperne di più sul ciclo di vita di un controller di visualizzazione e su tutti i metodi che vengono chiamati, ho scritto un articolo in cui sono descritti tutti loro.

Il controller di visualizzazione Modifica

Ora dobbiamo codificare il secondo controller della vista. Chiameremo questo EditViewController.

class EditViewController: UIViewController @IBOutlet weak var textView: UITextView! @IBOutlet weak var textField: UITextField! var modelController: ModelController! override func viewDidLoad () super.viewDidLoad () lascia quote = modelController.quote textView.text = quote.text textField.text = quote.author

Questo controller di visualizzazione è come il precedente:

  • Dispone di prese per la visualizzazione del testo e il campo di testo che l'utente utilizzerà per modificare la citazione.
  • Ha una proprietà per l'iniezione delle dipendenze dell'istanza del controllore del modello.
  • Compila la sua interfaccia utente prima di arrivare sullo schermo.

In questo caso, ho usato il viewDidLoad () metodo perché questo controller di visualizzazione viene visualizzato sullo schermo una sola volta.

Condivisione dello stato

Ora è necessario passare lo stato tra i due controller di visualizzazione e aggiornarlo quando l'utente modifica l'offerta.

Passiamo lo stato dell'app in preparare (per: mittente :) metodo di QuoteViewController. Questo metodo viene attivato dal seguito collegato quando l'utente tocca il Modifica preventivo pulsante.

class QuoteViewController: UIViewController @IBOutlet weak var quoteTextLabel: UILabel! @IBOutlet weak var quoteAuthorLabel: UILabel! var modelController: ModelController! override func viewWillAppear (_ animato: Bool) super.viewWillAppear (animato) let quote = modelController.quote quoteTextLabel.text = quote.text quoteAuthorLabel.text = quote.author override func prepare (per segue: UIStoryboardSegue, mittente: Any? ) if let editViewController = segue.destination as? EditViewController editViewController.modelController = modelController

Qui passiamo l'istanza del ModelController che mantiene lo stato dell'app. Questo è dove l'iniezione di dipendenza per il EditViewController succede.

Nel EditViewController, dobbiamo aggiornare lo stato alla quotazione appena inserita prima di tornare al precedente controller della vista. Possiamo farlo in un'azione connessa al Salvare pulsante:

class EditViewController: UIViewController @IBOutlet weak var textView: UITextView! @IBOutlet weak var textField: UITextField! var modelController: ModelController! override func viewDidLoad () super.viewDidLoad () lascia quote = modelController.quote textView.text = quote.text textField.text = quote.author @IBAction func save (_ mittente: AnyObject) let newQuote = Quote (testo: textView.text, autore: textField.text!) modelController.quote = newQuote respinge (animato: true, completion: nil)

Inizializza il controller del modello

Abbiamo quasi finito, ma potresti aver notato che ci manca ancora qualcosa: il QuoteViewController passa il ModelController al EditViewController attraverso l'iniezione di dipendenza. Ma chi dà questa istanza al QuoteViewController innanzitutto? Ricordare che quando si utilizza l'iniezione di dipendenza, un controller di visualizzazione non deve creare le proprie dipendenze. Questi devono venire dall'esterno.

Ma non c'è un controller di visualizzazione prima del QuoteViewController, perché questo è il primo controller di visualizzazione della nostra app. Abbiamo bisogno di qualche altro oggetto per creare il ModelController istanza e passarla al QuoteViewController.

Questo oggetto è il AppDelegate. Il ruolo del delegato dell'app è quello di rispondere ai metodi del ciclo di vita dell'app e configurare di conseguenza l'app. Uno di questi metodi è applicazione (_: didFinishLaunchingWithOptions :), che viene chiamato non appena l'app si avvia. È qui che creiamo l'istanza del ModelController e passalo al QuoteViewController:

class AppDelegate: UIResponder, UIApplicationDelegate var window: UIWindow? func application (_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool se lascia quoteViewController = window? .rootViewController as? QuoteViewController quoteViewController.modelController = ModelController () return true

La nostra app è ora completa. Ogni controller di visualizzazione ottiene l'accesso allo stato globale dell'app, ma non usiamo singoletti in nessun punto del nostro codice.

Puoi scaricare il progetto Xcode per questa app di esempio nel tutorial repo GitHub.

conclusioni

In questo articolo hai visto come usare i singleton per propagare lo stato in un'app per iOS è una cattiva pratica. I singleton creano molti problemi, nonostante siano molto facili da creare e utilizzare.

Abbiamo risolto il problema guardando più da vicino il pattern MVC e comprendendo le possibilità nascoste in esso. Tramite l'utilizzo di controller modello e iniezione di dipendenze, siamo stati in grado di propagare lo stato dell'app su tutti i controller di visualizzazione senza utilizzare singleton.

Questa è una semplice app di esempio, ma il concetto può essere generalizzato alle app di qualsiasi complessità. Questa è la migliore pratica standard per propagare lo stato nelle app iOS. Ora lo uso in ogni app che scrivo per i miei clienti.

Alcune cose da tenere a mente quando si espande il concetto in app più grandi:

  • Il controller del modello può salvare lo stato dell'app, ad esempio in un file. In questo modo, i nostri dati verranno ricordati ogni volta che chiudiamo l'app. È anche possibile utilizzare una soluzione di archiviazione più complessa, ad esempio Core Data. La mia raccomandazione è di mantenere questa funzionalità in un controller modello separato che si occupa solo dello storage. Il controller può quindi essere utilizzato dal controller del modello che mantiene lo stato dell'app.
  • In un'app con un flusso più complesso, avrai molti contenitori nel flusso dell'app. Si tratta in genere di controller di navigazione, con controller della barra delle schede occasionali. Il concetto di dipendenza da dipendenza si applica ancora, ma è necessario prendere in considerazione i contenitori. È possibile scavare nei controller della vista contenuti quando si esegue l'iniezione delle dipendenze o creare sottoclassi di contenitore personalizzate che passano sul controller del modello.
  • Se si aggiunge la rete alla propria app, questa dovrebbe andare anche in un controllore modello separato. Un controller di visualizzazione può eseguire una richiesta di rete attraverso questo controller di rete e quindi passare i dati risultanti al controller del modello che mantiene lo stato. Ricorda che il ruolo di un controller di visualizzazione è esattamente questo: agire come un oggetto colla che trasferisce i dati tra gli oggetti.

Resta sintonizzato per ulteriori suggerimenti e best practice per lo sviluppo di app iOS!