Det rätta sättet att dela del mellan Swift View Controllers

Vad du ska skapa

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.

Vad de flesta iOS-utvecklare blir felaktiga

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:

  • De introducerar många beroenden i dina klasser, vilket gör det svårare att ändra dem i framtiden.
  • De gör globalt tillstånd tillgängligt för någon del av din kod. Detta kan skapa komplexa interaktioner som är svåra att spåra och orsaka många oväntade fel.
  • De gör dina klasser mycket svåra att testa, eftersom du inte kan skilja dem enkelt från en singleton.

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.

Utöka MVC-mönstret

MVC-mönstret, i ett nötskal, säger att det finns tre lager i arkitekturen för en iOS-app:

  • Modellaget representerar data för en app.
  • Utsiktsskiktet visar information på skärmen och tillåter interaktion.
  • Styrskiktet fungerar som lim mellan de andra två skikten och flyttar data mellan dem.

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.

Propagating State Across View Controllers med hjälp av Dependency Injection

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

Model Controller

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.

Skapa användargränssnittet

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.

Koda en visningskontroller med beredskapsinjektion

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.

Redigeringsvisningskontrollen

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:

  • Den har uttag för textvyn och det textfält som användaren kommer att använda för att redigera citatet.
  • Den har en egenskap för beroendeinsprutningen av modellkontrollerns förekomst.
  • Den fyller användargränssnittet innan det kommer på skärmen.

I det här fallet använde jag viewDidLoad () metod eftersom den här kontrollenheten bara kommer en gång på skärmen.

Dela staten

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)

Initiera modellkontrollen

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.

Slutsatser

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:

  • Modulkontrollern kan spara tillståndet för appen, till exempel i en fil. På så sätt kommer våra data att komma ihåg varje gång vi stänger appen. Du kan också använda en mer komplex lagringslösning, till exempel Core Data. Min rekommendation är att behålla denna funktionalitet i en separat modellkontroll som endast tar hand om lagring. Den styrenheten kan då användas av modellkontrollern som håller tillståndet för appen.
  • I en app med ett mer komplicerat flöde kommer du att ha många behållare i ditt appflöde. Dessa är vanligtvis navigationsstyrare, med enstaka tabulatorstyrenhet. Begreppet beroendeinsprutning gäller fortfarande, men du måste ta hänsyn till behållarna. Du kan antingen gräva in i sina innehattade visningsstyrare när du utför beredskapsinjektionen eller skapa anpassade behållarens underklasser som passerar modellkontrollen på.
  • Om du lägger till nätverk i din app ska det också gå i en separat modellkontroll. En vyskontrollör kan utföra en nätverksförfrågan via denna nätverksstyrenhet och sedan överföra den resulterande data till modellstyrenheten som håller tillståndet. Kom ihåg att rollen som en kontroller är precis här: att fungera som ett limobjekt som skickar data runt mellan objekt.

Håll dig uppdaterad för mer iOS-apputvecklingstips och bästa praxis!