De juiste manier om de status te delen tussen Swift View Controllers

Wat je gaat creëren

Een paar jaar geleden, toen ik nog een werknemer was in een mobiel adviesbureau, werkte ik aan een app voor een grote investeringsbank. Grote bedrijven, met name banken, hebben meestal processen om ervoor te zorgen dat hun software veilig, robuust en onderhoudbaar is.

Onderdeel van dit proces was het verzenden van de code van de app die ik schreef ter beoordeling aan een derde partij. Dat stoorde me niet, omdat ik dacht dat mijn code onberispelijk was en dat het reviewbedrijf hetzelfde zou zeggen.

Toen hun reactie terugkeerde, was het vonnis anders dan ik dacht. Hoewel ze zeiden dat de kwaliteit van de code niet slecht was, wezen ze erop dat de code moeilijk te onderhouden en te testen was (unit-testen was toen nog niet erg populair in iOS-ontwikkeling).

Ik verwierp hun oordeel, denkend dat mijn code geweldig was en dat er op geen enkele manier verbetering mogelijk was. Ze moeten het gewoon niet begrijpen!

Ik had de typische ontwikkelaarshypris: we denken vaak dat wat we doen geweldig is en dat anderen het niet snappen. 

Achteraf gezien had ik het mis. Niet veel later begon ik te lezen over enkele best practices. Vanaf dat moment begonnen de problemen in mijn code te steken als een pijnlijke duim. Ik besefte dat ik, net als veel andere iOS-ontwikkelaars, bezweek voor enkele klassieke valkuilen van slechte codeermethoden.

Wat de meeste iOS-ontwikkelaars fout doen

Een van de meest voorkomende slechte praktijken bij het ontwikkelen van iOS's ontstaat wanneer status wordt doorgegeven tussen de view-controllers van een app. Ikzelf ben in het verleden in deze val gelopen.

Staatsverspreiding over view controllers is essentieel in elke iOS-app. Terwijl uw gebruikers door de schermen van uw app navigeren en ermee communiceren, moet u een algemene status behouden die alle wijzigingen bijhoudt die de gebruiker in de gegevens aanbrengt.

En dit is waar de meeste iOS-ontwikkelaars naar streven om de voor de hand liggende, maar onjuiste oplossing te vinden: het singleton-patroon.

Het singleton-patroon is zeer snel te implementeren, vooral in Swift, en het werkt goed. U hoeft alleen een statische variabele toe te voegen aan een klasse om een ​​gedeeld exemplaar van de klasse zelf te behouden en u bent klaar.

class Singleton static let shared = Singleton ()

Het is dan gemakkelijk om toegang te krijgen tot dit gedeelde exemplaar vanaf elke plek in uw code:

laat singleton = Singleton.gedeeld

Om deze reden denken veel ontwikkelaars dat ze de beste oplossing voor het probleem van staatsuitbreiding hebben gevonden. Maar ze hebben ongelijk.

Het singleton-patroon wordt eigenlijk beschouwd als een anti-patroon. Er zijn veel discussies hierover geweest in de ontwikkelingsgemeenschap. Zie bijvoorbeeld deze vraag over Stack Overflow.

In een notendop maken singletons deze problemen:

  • Ze introduceren veel afhankelijkheden in je lessen, waardoor het moeilijker wordt ze in de toekomst te veranderen.
  • Ze maken de globale status toegankelijk voor elk onderdeel van uw code. Dit kan complexe interacties maken die moeilijk te volgen zijn en veel onverwachte fouten veroorzaken.
  • Ze maken je lessen erg moeilijk om te testen, omdat je ze niet eenvoudig kunt scheiden van een singleton.

Op dit punt denken sommige ontwikkelaars: "Ah, ik heb een betere oplossing. Ik zal de gebruiken AppDelegate in plaats daarvan".

Het probleem is dat de AppDelegate klasse in iOS-apps is toegankelijk via de UiApplication gedeelde instantie:

laat appDelegate = UIApplication.shared.delegate

Maar de gedeelde instantie van UiApplication is zelf een singleton. Dus je hebt niets opgelost!

De oplossing voor dit probleem is injectie van afhankelijkheid. Afhankelijkheidsinjectie betekent dat een klasse zijn eigen afhankelijkheden niet ophaalt of maakt, maar deze van buitenaf ontvangt.

Om te zien hoe afhankelijkheidsinjectie in iOS-apps te gebruiken en hoe het staatsharing kan inschakelen, moeten we eerst een van de fundamentele architecturale patronen van iOS-apps opnieuw bekijken: het patroon Model-View-Controller.

Het MVC-patroon uitbreiden

Het MVC-patroon stelt in een notendop dat er drie lagen zijn in de architectuur van een iOS-app:

  • De modellaag vertegenwoordigt de gegevens van een app.
  • De weergavelaag toont informatie op het scherm en maakt interactie mogelijk.
  • De controllernaag fungeert als lijm tussen de andere twee lagen, waarbij gegevens tussen hen worden verplaatst.

De gebruikelijke weergave van het MVC-patroon is zoiets als dit:

Het probleem is dat dit diagram verkeerd is.

Dit "geheim" verbergt in een paar regels duidelijk in de documentatie van Apple:

"Men kan de MVC-rollen die door een object worden gespeeld, samenvoegen, waardoor een object bijvoorbeeld zowel de controller als de kijkrollen vervult. In dat geval zou het een view-controller worden genoemd. Op dezelfde manier kunt u ook model-controller-objecten hebben. "

Veel ontwikkelaars denken dat view controllers de enige controllers zijn die in een iOS-app bestaan. Om deze reden wordt er heel veel code in geschreven omdat er geen betere plek is. Dit is wat ontwikkelaars ertoe brengt om singletons te gebruiken wanneer ze status moeten doorgeven: het lijkt de enige mogelijke oplossing.

Uit de hierboven geciteerde lijnen is het duidelijk dat we een nieuwe entiteit kunnen toevoegen aan ons begrip van het MVC-patroon: de modelcontroller. Modelcontrollers gaan over het model van de app en vervullen de rollen die het model zelf niet zou moeten vervullen. Dit is eigenlijk hoe het bovenstaande schema eruit zou moeten zien:

Het perfecte voorbeeld van wanneer een modelcontroller nuttig is, is om de staat van de app te behouden. Het model moet alleen de gegevens van uw app vertegenwoordigen. De status van de app mag niet zijn zorg zijn.

Deze statusbehoud eindigt meestal binnen view controllers, maar nu hebben we een nieuwe en betere plaats om het te zeggen: een model controller. Deze modelcontroller kan vervolgens worden doorgegeven aan view-controllers als ze op het scherm verschijnen via afhankelijkheidsinjectie.

We hebben het singleton anti-patroon opgelost. Laten we onze oplossing in de praktijk bekijken met een voorbeeld.

Propagerende status over controllers door middel van afhankelijkheid Injectie

We gaan een eenvoudige app schrijven om een ​​concreet voorbeeld te zien van hoe dit werkt. De app zal je favoriete quote op één scherm tonen en je kunt de quote op een tweede scherm bewerken.

Dit betekent dat onze app twee view-controllers nodig heeft, die de status moeten delen. Nadat u ziet hoe deze oplossing werkt, kunt u het concept uitbreiden naar apps van elke grootte en complexiteit.

Om te beginnen hebben we een model nodig om de gegevens weer te geven, wat in ons geval een citaat is. Dit kan gedaan worden met een eenvoudige struct:

struct Quote let text: String let author: String

De Model Controller

Vervolgens moeten we een modelcontroller maken die de staat van de app bevat. Deze modelcontroller moet een klasse zijn. Dit komt omdat we een enkele instantie nodig hebben die we doorgeven aan al onze view-controllers. Waardetypes zoals structs worden gekopieerd wanneer we ze doorgeven, dus ze zijn duidelijk niet de juiste oplossing.

Al onze modelcontroller moet in ons voorbeeld een eigenschap zijn waar het de huidige quote kan behouden. Maar in grotere apps kunnen modelcontrollers natuurlijk complexer zijn dan dit:

class ModelController var quote = Quote (tekst: "Twee dingen zijn oneindig: het universum en de menselijke stompzinnigheid, en ik ben niet zeker van het universum.", auteur: "Albert Einstein")

Ik heb een standaardwaarde toegewezen aan de citaat eigendom, dus we hebben al iets dat op het scherm wordt weergegeven wanneer de app wordt gestart. Dit is niet nodig en u zou de eigenschap kunnen declareren als optioneel geïnitialiseerd nul, als u wilt dat uw app wordt gestart met een lege status.

Maak de gebruikersinterface

We hebben nu de modelcontroller, die de staat van onze app zal bevatten. Vervolgens hebben we de beeldcontrollers nodig die de schermen van onze app vertegenwoordigen.

Eerst maken we hun gebruikersinterfaces. Dit is hoe de twee view-controllers in het storyboard van de app kijken.

De interface van de first view-controller bestaat uit een aantal labels en een knop, in combinatie met eenvoudige beperkingen voor de automatische lay-out. (U kunt meer lezen over automatische lay-out hier op Envato Tuts +.)

De interface van de tweede weergavecontroller is hetzelfde, maar heeft een tekstweergave om de tekst van de aanhalingstekst te bewerken en een tekstveld om de auteur te bewerken.

De twee view controllers zijn verbonden door een enkele modale presentatie-segue, die afkomstig is van de Bewerk citaat knop.

U kunt de interface en de beperkingen van de view-controllers in de GitHub-repo verkennen.

Codeer een weergavecontroller met afhankelijkheidsinjectie

We moeten nu onze viewcontrollers coderen. Het belangrijkste dat we hier moeten onthouden, is dat ze de instance van de modelcontroller van buitenaf moeten ontvangen, via afhankelijkheidsinjectie. Dus moeten ze een woning voor dit doel blootstellen.

var modelController: ModelController!

We kunnen onze first view-controller bellen QuoteViewController. Deze view-controller heeft een aantal verkooppunten nodig voor de labels voor de quote en de auteur in de interface.

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

Wanneer deze view-controller op het scherm verschijnt, vullen we de interface om de huidige quote weer te geven. We plaatsen de code om dit in de controller's te doen viewWillAppear (_ :) methode.

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

We hadden deze code in de viewDidLoad () methode in plaats daarvan, wat vrij gebruikelijk is. Het probleem is echter dat viewDidLoad () wordt slechts één keer aangeroepen wanneer de view-controller wordt gemaakt. In onze app moeten we de gebruikersinterface bijwerken van QuoteViewController elke keer dat het op het scherm verschijnt. Dit komt omdat de gebruiker de quote op het tweede scherm kan bewerken. 

Dit is de reden waarom we de viewWillAppear (_ :) methode in plaats van viewDidLoad (). Op deze manier kunnen we de gebruikersinterface van de weergaveregelaar bijwerken elke keer dat deze op het scherm verschijnt. Als je meer wilt weten over de levenscyclus van een view controller en alle methoden die worden genoemd, heb ik een artikel geschreven waarin ze allemaal worden beschreven.

De bewerkingsweergave-controller

We moeten nu de tweede view controller coderen. We zullen deze noemen EditViewController.

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

Deze weergavecontroller is als de vorige:

  • Het heeft verkooppunten voor de tekstweergave en het tekstveld dat de gebruiker zal gebruiken om de quote te bewerken.
  • Het heeft een eigenschap voor de injectie van de afhankelijkheid van de exemplaar van de modelcontroller.
  • Het vult de gebruikersinterface voordat het op het scherm verschijnt.

In dit geval heb ik de viewDidLoad () methode omdat deze weergaveregelaar slechts één keer op het scherm wordt weergegeven.

De staat delen

We moeten nu de status doorgeven tussen de twee view controllers en deze bijwerken wanneer de gebruiker de quote bewerkt.

We geven de app-status door in de bereiden (voor: afzender :) methode van QuoteViewController. Deze methode wordt geactiveerd door de verbonden segue wanneer de gebruiker op de knop tikt Bewerk citaat knop.

class QuoteViewController: UIViewController @IBOutlet weak var quoteTextLabel: UILabel! @IBOutlet weak var quoteAuthorLabel: UILabel! var modelController: ModelController! override func viewWillAppear (_ geanimeerd: Bool) super.viewWillAppear (geanimeerd) laat citaat = modelController.quote quoteTextLabel.text = quote.text quoteAuthorLabel.text = quote.author overschrijven func voorbereiden (voor segue: UIStoryboardSegue, afzender: Any? ) als editControlController = segue.destination als? EditViewController editViewController.modelController = modelController

Hier geven we de instance van de ModelController die de staat van de app houdt. Dit is waar de afhankelijkheid injectie voor de EditViewController gebeurt.

In de EditViewController, we moeten de staat bijwerken naar de nieuw ingevoerde quote voordat we teruggaan naar de vorige view controller. We kunnen dit doen in een actie verbonden met de Opslaan knop:

class EditViewController: UIViewController @IBOutlet weak var textView: UITextView! @IBOutlet weak var textField: UITextField! var modelController: ModelController! override func viewDidLoad () super.viewDidLoad () laat quote = modelController.quote textView.text = quote.text textField.text = quote.author @IBAction func save (_ afzender: AnyObject) let newQuote = Offerte (tekst: textView.text, auteur: textField.text!) modelController.quote = newQuote dismiss (geanimeerd: true, completion: nil)

Initialiseer de Model Controller

We zijn bijna klaar, maar je hebt misschien gemerkt dat we nog steeds iets missen: het QuoteViewController passeert de ModelController naar de EditViewController door afhankelijkheid injectie. Maar wie geeft deze instantie aan de QuoteViewController in de eerste plaats? Houd er rekening mee dat wanneer een afhankelijkheidsinjectie wordt gebruikt, een view-controller geen eigen afhankelijkheden moet maken. Deze moeten van buiten komen.

Maar er is geen view controller voor de QuoteViewController, omdat dit de eerste beeldcontroller van onze app is. We hebben een ander object nodig om het te maken ModelController voorbeeld en om het door te geven aan de QuoteViewController.

Dit object is het AppDelegate. De rol van de app-deelnemer is om te reageren op de levenscyclusmethoden van de app en de app dienovereenkomstig te configureren. Een van deze methoden is applicatie (_: didFinishLaunchingWithOptions :), die wordt gebeld zodra de app wordt gestart. Dat is waar we het exemplaar van de maken ModelController en geef het door aan de QuoteViewController:

class AppDelegate: UIResponder, UIAupplicationDelegate var window: UIWindow? func application (_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool if letificateViewController = window?. RootViewController als? QuoteViewController quoteViewController.modelController = ModelController () return true

Onze app is nu voltooid. Elke view-controller krijgt toegang tot de algemene status van de app, maar we gebruiken geen singletons overal in onze code.

Je kunt het Xcode-project voor deze voorbeeldapp downloaden in de tutorial GitHub-repo.

conclusies

In dit artikel heb je gezien hoe het gebruik van singletons om de status in een iOS-app te verspreiden een slechte gewoonte is. Singletons zorgen voor veel problemen, ondanks dat ze heel gemakkelijk kunnen worden gemaakt en gebruikt.

We hebben het probleem opgelost door het MVC-patroon nauwkeuriger te bekijken en de verborgen mogelijkheden te begrijpen. Door het gebruik van modelcontrollers en afhankelijkheidsinjectie waren we in staat om de status van de app over alle viewcontrollers te verspreiden zonder gebruik te maken van singletons.

Dit is een eenvoudige voorbeeld-app, maar het concept kan worden gegeneraliseerd naar apps van elke complexiteit. Dit is de standaardbest practice voor het verspreiden van de status in iOS-apps. Ik gebruik het nu in elke app die ik voor mijn klanten schrijf.

Een paar dingen om rekening mee te houden wanneer u het concept uitbreidt naar grotere apps:

  • De modelcontroller kan de status van de app opslaan, bijvoorbeeld in een bestand. Op deze manier worden onze gegevens onthouden telkens wanneer we de app sluiten. U kunt ook een complexere opslagoplossing gebruiken, bijvoorbeeld Core Data. Mijn aanbeveling is om deze functionaliteit te behouden in een aparte modelcontroller die alleen voor opslag zorgt. Die controller kan dan worden gebruikt door de modelcontroller die de staat van de app bewaart.
  • In een app met een complexere stroom heb je veel containers in je app-flow. Dit zijn meestal navigatiecontrollers, met af en toe een tabbalkcontroller. Het concept afhankelijkheidsinjectie is nog steeds van toepassing, maar u moet wel rekening houden met de containers. U kunt in de view-controllers graven wanneer u de afhankelijkheidsinjectie uitvoert, of u kunt aangepaste containersubklassen maken die de modelcontroller doorgeven op.
  • Als u netwerken aan uw app toevoegt, zou dit ook in een afzonderlijke modelcontroller moeten gaan. Een weergaveregelaar kan via deze netwerkcontroller een netwerkverzoek uitvoeren en de resulterende gegevens doorgeven aan de modelcontroller die de status bewaart. Houd er rekening mee dat de rol van een view-controller precies dit is: om te fungeren als een lijmobject dat gegevens tussen objecten doorgeeft.

Blijf op de hoogte voor meer tips en praktische tips voor het ontwikkelen van iOS-apps!