För några år sedan, när jag fortfarande var anställd i mobilrådgivning, arbetade jag på en app för en stor investeringsbank. Stora företag, särskilt banker, har vanligtvis processer på plats för att säkerställa att deras programvara är säker, robust och underhållbar.
En del av den här processen var att skicka koden till appen jag skrev till en tredje part för granskning. Det störde inte mig, för jag trodde att min kod var oklanderlig och att granskningsföretaget skulle säga detsamma.
När deras svar kom tillbaka var domen annorlunda än vad jag trodde. Även om de sa att kvaliteten på koden inte var dålig, pekade de på att koden var svår att underhålla och testa (enhetstestning var inte särskilt populär i iOS-utveckling då).
Jag avskedade sin dom och trodde att min kod var stor och det fanns inget sätt att det skulle kunna förbättras. De måste bara inte förstå det!
Jag hade den typiska utvecklarnahubrisen: vi tycker ofta att det vi gör är bra och andra inte får det.
I efterhand hade jag fel. Inte mycket senare började jag läsa om några bästa praxis. Från och med då började problemen i min kod sticka ut som en öm tumme. Jag insåg att, liksom många iOS-utvecklare, jag hade succumbed till några klassiska fallgropar av dålig kodning.
En av de vanligaste iOS-utvecklingen är dåliga rutiner som uppkommer när man skickar tillstånd mellan visningskontrollerna i en app. Jag har själv fallit i denna fälla i det förflutna.
Statlig spridning över olika kontroller är avgörande för alla iOS-appar. Eftersom dina användare navigerar via skärmen i din app och interagerar med det måste du hålla ett globalt tillstånd som spårar alla förändringar som användaren gör till data.
Och det är här de flesta iOS-utvecklare når upp för den uppenbara, men felaktiga lösningen: singleton-mönstret.
Singleton-mönstret är väldigt snabbt att implementera, särskilt i Swift, och det fungerar bra. Du behöver bara lägga till en statisk variabel i en klass för att behålla en delad förekomst av klassen själv, och du är klar.
klass Singleton static let shared = Singleton ()
Då är det enkelt att komma åt den här delade instansen, var som helst i din kod:
låt singleton = Singleton.shared
Av denna anledning tror många utvecklare att de hittade den bästa lösningen på problemet med statlig förökning. Men de har fel.
Singleton-mönstret anses faktiskt som ett anti-mönster. Det har funnits många diskussioner om detta i utvecklingssamhället. Se till exempel den här Stack Overflow-frågan.
I ett nötskal skapar singletoner dessa problem:
Vid denna tidpunkt tror vissa utvecklare: "Ah, jag har en bättre lösning. Jag kommer att använda AppDelegate
istället".
Problemet är att AppDelegate
klass i iOS-appar öppnas via UIApplication
delad instans:
låt appDelegate = UIApplication.shared.delegate
Men den gemensamma förekomsten av UIApplication
är själv en singleton. Så du har inte löst något!
Lösningen på detta problem är beroendeinsprutning. Dependensinjektion innebär att en klass inte hämtar eller skapar egna beroenden, men det tar emot dem från utsidan.
För att se hur man använder beroendeinsprutning i iOS-appar och hur det kan aktivera delning av staten, behöver vi först se en av de grundläggande arkitektoniska mönster i iOS-appar: Model-View-Controller-mönstret.
MVC-mönstret, i ett nötskal, säger att det finns tre lager i arkitekturen för en iOS-app:
Den vanliga representationen av MVC-mönstret är något som här:
Problemet är att detta diagram är fel.
Den här "hemligheten" gömmer sig i vanligt syn på ett par rader i Apples dokumentation:
"Man kan slå samman de MVC-roller som spelas av ett objekt, göra ett objekt, till exempel, uppfylla både kontrollenheten och visa roller-i vilket fall det skulle kallas en vykontroll. På samma sätt kan du också ha modellkontrollerns objekt. "
Många utvecklare tror att visningsstyrare är de enda controllers som finns i en iOS-app. Av denna anledning slutar en massa kod skrivas inuti dem för brist på en bättre plats. Det här får utvecklare att använda singletoner när de behöver sprida tillstånd: det verkar som den enda möjliga lösningen.
Av ovanstående linjer är det tydligt att vi kan lägga till en ny enhet till vår förståelse för MVC-mönstret: modellkontrollen. Modellkontrollanter hanterar appens modell, uppfyller de roller som modellen inte bör uppfylla. Detta är faktiskt hur ovanstående schema ska se ut:
Det perfekta exemplet när en modellstyrenhet är användbar är att behålla appens tillstånd. Modellen ska bara representera data för din app. Appens tillstånd borde inte vara oroande.
Det här tillståndet håller vanligtvis slut på insynsregulatorer, men nu har vi en ny och bättre plats att uttrycka det: en modellkontrollant. Denna modellstyrenhet kan sedan skickas för att se kontroller som de kommer på skärmen genom beroendeinsprutning.
Vi har löst singleton anti-mönstret. Låt oss se vår lösning i praktiken med ett exempel.
Vi ska skriva en enkel app för att se ett konkret exempel på hur det fungerar. Appen kommer att visa din favoritsats på en skärm och låter dig redigera citatet på en andra skärm.
Det betyder att vår app behöver två visningsstyrare, som måste dela med sig av staten. När du ser hur den här lösningen fungerar kan du expandera konceptet till appar av vilken storlek som helst och komplexitet.
För att starta behöver vi en modelltyp för att representera data, vilket i vårt fall är ett citat. Detta kan göras med en enkel struktur:
struct citat låt text: String låt författare: String
Vi behöver då skapa en modellkontrollant som håller tillståndet för appen. Denna modellkontrollant behöver vara en klass. Detta beror på att vi behöver en enda instans som vi kommer att överföra till alla våra kontroller. Värdetyper som strukturer kopieras när vi skickar dem runt, så de är helt klart inte rätt lösning.
Alla våra modellkontrollbehov i vårt exempel är en egenskap där den kan behålla den aktuella citat. Men, naturligtvis, i större appar kan modellkontrollanter vara mer komplexa än detta:
klassen ModelController var quote = Citat (text: "Två saker är oändliga: universum och mänsklighetens dumhet, och jag är inte säker på universum.", författare: "Albert Einstein")
Jag tilldelade ett standardvärde till Citat egendom så vi kommer redan ha något att visa på skärmen när appen lanseras. Detta är inte nödvändigt, och du kan förklara egenskapen som en valfri initialiserad till noll, om du vill att din app ska startas med ett tomt tillstånd.
Vi har nu modellkontrollanten, som kommer att innehålla tillståndet för vår app. Därefter behöver vi de visningsstyrare som representerar skärmen i vår app.
Först skapar vi sina användargränssnitt. Så här ser de två bildkontrollerna inuti appens storyboard.
Gränssnittet för den första bildkontrollen består av ett par etiketter och en knapp som kombineras med enkla autoutläggningsbegränsningar. (Du kan läsa mer om automatisk layout här på Envato Tuts +.)
Gränssnittet för den andra bildkontrollen är detsamma men har en textvy för att redigera texten i citatet och ett textfält för att redigera författaren.
De två bildkontrollerna är anslutna med en enda modalpresentationssigue, som härstammar från Redigera citat knapp.
Du kan utforska gränssnittet och begränsningarna för visningskontrollerna i GitHub repo.
Vi behöver nu koda våra kontroller. Det viktiga som vi behöver komma ihåg här är att de behöver ta emot modellkontrollerns instans från utsidan, genom beroendeinsprutning. Så de måste exponera en egendom för detta ändamål.
var modelController: ModelController!
Vi kan ringa vår första bildkontrollenhet QuoteViewController
. Denna visningskontroller behöver ett par uttag till etiketterna för citatet och författaren i dess gränssnitt.
klass QuoteViewController: UIViewController @IBOutlet weak var quoteTextLabel: UILabel! @IBOutlet svag var quoteAuthorLabel: UILabel! var modelController: ModelController!
När den här skärmkontrollen kommer på skärmen fyller vi dess gränssnitt för att visa aktuellt citat. Vi lägger koden för att göra detta i regulatorns viewWillAppear (_ :)
metod.
klass QuoteViewController: UIViewController @IBOutlet weak var quoteTextLabel: UILabel! @IBOutlet svag var quoteAuthorLabel: UILabel! var modelController: ModelController! åsidosätta func viewWillAppear (_ animerad: Bool) super.viewWillAppear (animerad) låt citat = modelController.quote quoteTextLabel.text = quote.text quoteAuthorLabel.text = quote.author
Vi kunde ha lagt den här koden inuti viewDidLoad ()
metod istället, vilket är ganska vanligt. Problemet är dock det viewDidLoad ()
kallas endast en gång när bildkontrollen skapas. I vår app behöver vi uppdatera användargränssnittet för QuoteViewController
varje gång det kommer på skärmen. Detta beror på att användaren kan redigera citatet på den andra skärmen.
Det är därför vi använder viewWillAppear (_ :)
metod istället för viewDidLoad ()
. På detta sätt kan vi uppdatera visningskontrollens användargränssnitt varje gång det visas på skärmen. Om du vill veta mer om en bildkontrollens livscykel och alla metoder som kallas, skrev jag en artikel som beskriver dem alla.
Vi måste nu koda andra kontrollenheten. Vi kommer att ringa den här EditViewController
.
klass EditViewController: UIViewController @IBOutlet weak var textView: UITextView! @IBOutlet svars var textField: UITextField! var modelController: ModelController! åsidosätta func viewDidLoad () super.viewDidLoad () låt citat = modelController.quote textView.text = quote.text textField.text = quote.author
Denna bildkontroll är som den föregående:
I det här fallet använde jag viewDidLoad ()
metod eftersom den här kontrollenheten bara kommer en gång på skärmen.
Vi måste nu överföra staten mellan de två visningsstyrarna och uppdatera den när användaren ändrar citatet.
Vi skickar appstaten i framställa (för: avsändare :)
metod av QuoteViewController
. Denna metod utlöses av den anslutna seggen när användaren tappar på Redigera citat knapp.
klass QuoteViewController: UIViewController @IBOutlet weak var quoteTextLabel: UILabel! @IBOutlet svag var quoteAuthorLabel: UILabel! var modelController: ModelController! åsidosätta func viewWillAppear (_ animerad: Bool) super.viewWillAppear (animerad) låt citat = modelController.quote quoteTextLabel.text = quote.text quoteAuthorLabel.text = quote.author åsidosätta func preparation (för segue: UIStoryboardSegue, avsändare: Any? ) om låt redigeraViewController = segue.destination som? EditViewController editViewController.modelController = modelController
Här passerar vi fram förekomsten av ModelController
som håller tillståndet för appen. Det är här beroendet av injektionen för EditViewController
händer.
I EditViewController
, Vi måste uppdatera staten till det nyinmatade citatet innan vi går tillbaka till föregående visningsregulator. Vi kan göra detta i en åtgärd som är kopplad till Spara knapp:
klass EditViewController: UIViewController @IBOutlet weak var textView: UITextView! @IBOutlet svars var textField: UITextField! var modelController: ModelController! åsidosätta func viewDidLoad () super.viewDidLoad () låt citat = modelController.quote textView.text = quote.text textField.text = quote.author @IBAction func spara (_ avsändare: AnyObject) låt newQuote = Citat (text: textView.text, author: textField.text!) modelController.quote = newQuote dismiss (animerad: true, completion: nil)
Vi är nästan färdiga, men du kanske har märkt att vi fortfarande saknar något: QuoteViewController
passerar ModelController
till EditViewController
genom beroendeinsprutning. Men vem ger denna instans till QuoteViewController
för det första? Kom ihåg att när man använder beroendeinsprutning, ska en bildkontroll inte skapa egna beroenden. Dessa måste komma från utsidan.
Men det finns ingen bildkontroll före QuoteViewController
, eftersom det här är den första visningskontrollen i vår app. Vi behöver ett annat objekt för att skapa ModelController
instans och att överföra det till QuoteViewController
.
Detta objekt är AppDelegate
. Appdelegationens roll är att svara på appens livscykelmetoder och konfigurera appen i enlighet med detta. En av dessa metoder är applikations (_: didFinishLaunchingWithOptions :)
, som kallas så snart appen lanseras. Det är där vi skapar förekomsten av ModelController
och skicka den till QuoteViewController
:
klass AppDelegate: UIResponder, UIApplicationDelegate var fönster: UIWindow? func application (_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool om låt quoteViewController = fönster? .rootViewController som? QuoteViewController quoteViewController.modelController = ModelController () returnera true
Vår app är nu klar. Varje visningscontroller får tillgång till appens globala tillstånd, men vi använder inte singletoner någonstans i vår kod.
Du kan ladda ner Xcode-projektet för det här exemplet i handledningen GitHub repo.
I den här artikeln har du sett hur man använder singletoner för att sprida staten i en iOS-app är en dålig övning. Singletons skapar många problem, trots att det är väldigt lätt att skapa och använda.
Vi löste problemet genom att titta närmare på MVC-mönstret och förstå möjligheterna dolda i det. Genom användningen av modellkontrollers och beredskapsinjektion kunde vi sprida tillståndet för appen över alla kontroller utan att använda singletoner.
Det här är ett enkelt exempelapp, men konceptet kan generaliseras till appar av all komplexitet. Detta är standard bästa praxis att sprida tillstånd i iOS-appar. Jag använder den nu i varje app jag skriver för mina kunder.
Några saker att tänka på när du utvidgar konceptet till större appar:
Håll dig uppdaterad för mer iOS-apputvecklingstips och bästa praxis!