Den riktige måten å dele stat mellom Swift View Controllers

Hva du skal skape

For noen år siden, da jeg fortsatt var en ansatt i en mobilrådgivning, jobbet jeg med en app for en stor investeringsbank. Store selskaper, spesielt banker, har vanligvis prosesser på plass for å sikre at programvaren deres er sikker, robust og vedlikeholdsbar.

En del av denne prosessen involverte å sende koden til appen jeg skrev til en tredjepart for gjennomgang. Det forstyrret meg ikke, fordi jeg trodde at koden min var upåklagelig, og at revisjonsfirmaet ville si det samme.

Da deres svar kom tilbake, var dommen annerledes enn jeg trodde. Selv om de sa at kvaliteten på koden ikke var dårlig, pekte de på at koden var vanskelig å vedlikeholde og teste (enhetstesting var ikke veldig populær i IOS-utvikling da).

Jeg avviste sin dom, og tenkte at koden min var flott, og det var ingen måte det kunne bli bedre. De må bare ikke forstå det!

Jeg hadde den typiske utvikleren hubris: vi tror ofte at det vi gjør er flott og andre ikke får det. 

Etterpå hadde jeg feil. Ikke mye senere begynte jeg å lese om noen gode metoder. Fra da av begynte problemene i min kode å stikke ut som en sår tommel. Jeg innså at, som mange iOS-utviklere, hadde jeg gitt etter noen klassiske fallgruver med dårlig kodingspraksis.

Hva de fleste iOS-utviklere får feil

En av de vanligste iOS-utviklingen er dårlig praksis når det går over tilstand mellom visningsstyrerne i en app. Jeg har selv fallet inn i denne fellen i fortiden.

Statlig forplantning på tvers av visningskontrollere er viktig i alle iOS-apper. Som brukere navigerer gjennom skjermene i appen din og samhandler med den, må du beholde en global tilstand som følger alle endringene brukeren gjør til dataene.

Og det er her de fleste iOS-utviklere kommer til den åpenbare, men feilløsningen: singleton-mønsteret.

Singleton-mønsteret er veldig raskt å implementere, spesielt i Swift, og det fungerer bra. Du må bare legge til en statisk variabel i en klasse for å holde en delt forekomst av klassen selv, og du er ferdig.

klassen Singleton static let shared = Singleton ()

Det er så enkelt å få tilgang til denne delte forekomsten fra hvor som helst i koden din:

la singleton = Singleton.shared

Av denne grunn tror mange utviklere at de fant den beste løsningen på problemet med statlig forplantning. Men de har feil.

Singleton-mønsteret er faktisk betraktet som et anti-mønster. Det har vært mange diskusjoner om dette i utviklingssamfunnet. Se for eksempel dette Stack Overflow-spørsmålet.

I et nøtteskall skaper singletoner disse problemene:

  • De introduserer mange avhengigheter i klassene dine, noe som gjør det vanskeligere å endre dem i fremtiden.
  • De gjør global status tilgjengelig for alle deler av koden din. Dette kan skape komplekse samspill som er vanskelig å spore og forårsake mange uventede feil.
  • De gjør klassene dine svært vanskelig å teste, siden du ikke kan skille dem enkelt fra en singleton.

På dette punktet tenker noen utviklere: "Ah, jeg har en bedre løsning. Jeg vil bruke AppDelegate i stedet".

Problemet er at AppDelegate klassen i iOS-apper er tilgjengelig gjennom UIApplication delt eksempel:

la appDelegate = UIApplication.shared.delegate

Men den delte forekomsten av UIApplication er selv en singleton. Så du har ikke løst noe!

Løsningen på dette problemet er avhengighetsinjeksjon. Avhengighetsinjeksjon betyr at en klasse ikke henter eller skaper sine egne avhengigheter, men mottar dem fra utsiden.

Hvis du vil se hvordan du bruker avhengighetsinjeksjon i iOS-apper og hvordan den kan aktivere deling av delstaten, må vi først gå tilbake til et av de grunnleggende arkitektoniske mønstrene av iOS-apper: Modell-View-Controller-mønsteret.

Utvide MVC-mønsteret

MVC-mønsteret, i et nøtteskall, sier at det er tre lag i arkitekturen til en iOS-app:

  • Modellaget representerer dataene i en app.
  • Visningslaget viser informasjon på skjermen og tillater samhandling.
  • Kontrollerlaget virker som lim mellom de to andre lagene, flytting av data mellom dem.

Den vanlige representasjonen av MVC-mønsteret er noe som dette:

Problemet er at dette diagrammet er feil.

Denne "hemmelige" gjemmer seg i vanlig øye i et par linjer i Apples dokumentasjon:

"Man kan slå sammen MVC-rollene som spilles av et objekt, og lage et objekt, for eksempel, oppfylle både kontrolleren og vise roller - i så fall vil det bli kalt en visningskontroller. På samme måte kan du også ha modellkontrollerobjekter. "

Mange utviklere tror at visningskontrollere er de eneste kontrollerne som finnes i en iOS-app. Av denne grunn slutter en masse kode å bli skrevet inn i dem for mangel på et bedre sted. Dette er det som gir utviklere å bruke singletoner når de trenger å formere tilstand: det virker som den eneste mulige løsningen.

Fra linjene som er angitt ovenfor, er det klart at vi kan legge til en ny enhet til vår forståelse av MVC-mønsteret: modellkontrolleren. Modellkontrollere håndterer modellen av appen, oppfyller rollene som modellen selv ikke bør oppfylle. Dette er faktisk hvordan den ovennevnte ordningen skal se ut:

Det perfekte eksempelet på når en modellkontroller er nyttig, er for å holde appens tilstand. Modellen skal bare representere dataene i appen din. Appens tilstand burde ikke være dens bekymring.

Denne tilstandsbestemmelsen slutter som regel innenfor visningskontrollere, men nå har vi et nytt og bedre sted å si det: en modellkontroller. Denne modellkontrolleren kan da sendes for å se kontroller da de kommer på skjermen gjennom avhengighetsinjeksjon.

Vi har løst singleton anti-mønsteret. La oss se vår løsning i praksis med et eksempel.

Propagating State Over View Controllers Ved hjelp av Dependency Injection

Vi skal skrive en enkel app for å se et konkret eksempel på hvordan dette fungerer. Appen kommer til å vise favorittnotatet ditt på en skjerm, og lar deg redigere sitatet på en annen skjerm.

Dette betyr at vår app trenger to visningskontrollere, som må dele tilstand. Når du ser hvordan denne løsningen fungerer, kan du utvide konseptet til programmer av hvilken som helst størrelse og kompleksitet.

For å starte, trenger vi en modelltype for å representere dataene, som i vårt tilfelle er et sitat. Dette kan gjøres med en enkel struktur:

struct Sitat la tekst: String la forfatter: String

Modellkontrolleren

Vi må da opprette en modellkontroller som holder tilstanden til appen. Denne modellkontrolleren må være en klasse. Dette skyldes at vi trenger en enkelt forekomst som vi vil overføre til alle våre visningskontrollere. Verditypene som strukturer blir kopiert når vi sender dem rundt, så de er tydeligvis ikke den riktige løsningen.

Alle våre modellregulatorbehov i vårt eksempel er en eiendom der den kan beholde dagens tilbud. Men selvfølgelig, i større applikasjoner kan modellkontrollere være mer komplekse enn dette:

klassen ModelController var quote = Sitat (tekst: "To ting er uendelige: universet og menneskets dumhet, og jeg er ikke sikker på universet.", forfatter: "Albert Einstein")

Jeg tilordnet en standardverdi til sitat eiendom slik at vi allerede har noe å vise på skjermen når appen starter. Dette er ikke nødvendig, og du kan erklære eiendommen for å være en valgfri initialisert til nil, hvis du ønsker at appen skal starte med en tom tilstand.

Opprett brukergrensesnittet

Vi har nå modellkontrolleren, som vil inneholde tilstanden til vår app. Deretter trenger vi visningskontrollerne som representerer skjermene i appen vår.

Først oppretter vi brukergrensesnitt. Slik ser de to visningskontrollene inn i appens storyboard.

Grensesnittet til den første visningskontrolleren består av et par etiketter og en knapp, satt sammen med enkle automatisk oppsettbegrensninger. (Du kan lese mer om automatisk oppsett her på Envato Tuts +.)

Grensesnittet til den andre visningsregulatoren er det samme, men har en tekstvisning for å redigere teksten i sitatet og et tekstfelt for å redigere forfatteren.

De to visningskontrollerne er forbundet med en enkelt modal presentasjonssigue, som stammer fra Rediger sitat knapp.

Du kan utforske grensesnittet og begrensningene til visningskontrollerne i GitHub repo.

Kode en visningskontroller med avhengighetsinjeksjon

Vi må nå kode våre kontrollører. Den viktige tingen vi må huske på her er at de trenger å motta modellkontrollens instans fra utsiden, gjennom avhengighetsinjeksjon. Så de trenger å avsløre en eiendom for dette formålet.

var modelController: ModelController!

Vi kan ringe vår første visningskontroller QuoteViewController. Denne visningskontrolleren trenger et par uttak til etikettene for sitatet og forfatteren i grensesnittet.

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

Når denne visningen kontrolleren kommer på skjermen, fyller vi grensesnittet for å vise det nåværende sitatet. Vi legger koden til å gjøre dette i kontrolleren viewWillAppear (_ :) metode.

klasse QuoteViewController: UIViewController @IBOutlet weak var quoteTextLabel: UILabel! @IBOutlet svak var quoteAuthorLabel: UILabel! var modelController: ModelController! overstyr func viewWillAppear (_ animert: Bool) super.viewWillAppear (animert) la quote = modelController.quote quoteTextLabel.text = quote.text quoteAuthorLabel.text = quote.author

Vi kunne ha satt denne koden inne i viewDidLoad () metode i stedet, noe som er ganske vanlig. Problemet er imidlertid det viewDidLoad () kalles bare en gang, når visningsregulatoren er opprettet. I vår app må vi oppdatere brukergrensesnittet til QuoteViewController hver gang den kommer på skjermen. Dette skyldes at brukeren kan redigere sitatet på den andre skjermen. 

Det er derfor vi bruker viewWillAppear (_ :) metode istedenfor viewDidLoad (). På den måten kan vi oppdatere visningskontrollens brukergrensesnitt hver gang det vises på skjermen. Hvis du vil vite mer om en visningskontrollers livssyklus og alle metodene som blir kalt, skrev jeg en artikkel som beskriver dem alle.

Redigeringsvisningskontrollen

Vi trenger nå å kode den andre visningskontrollen. Vi vil ringe denne EditViewController.

klasse EditViewController: UIViewController @IBOutlet weak var textView: UITextView! @IBOutlet svak var textField: UITextField! var modelController: ModelController! overstyr func viewDidLoad () super.viewDidLoad () la quote = modelController.quote textView.text = quote.text textField.text = quote.author

Denne visningskontrolleren er som den forrige:

  • Den har utsalgssteder for tekstvisningen og tekstfeltet brukeren vil bruke til å redigere sitatet.
  • Den har en egenskap for avhengighetsinjeksjonen av modellkontrolleren.
  • Den fyller brukergrensesnittet før du kommer på skjermen.

I dette tilfellet brukte jeg viewDidLoad () metode fordi denne visningskontrolleren bare kommer på skjermen en gang.

Deling av staten

Vi må nå passere staten mellom de to visningskontrollerne og for å oppdatere den når brukeren redigerer sitatet.

Vi sender app-staten i forberede (for avsender- :) Metode av QuoteViewController. Denne metoden utløses av den forbundne segue når brukeren tapper på Rediger sitat knapp.

klasse QuoteViewController: UIViewController @IBOutlet weak var quoteTextLabel: UILabel! @IBOutlet svak var quoteAuthorLabel: UILabel! var modelController: ModelController! overstyr func viewWillAppear (_ animert: Bool) super.viewWillAppear (animert) la quote = modelController.quote quoteTextLabel.text = quote.text quoteAuthorLabel.text = quote.author overstyre func preparation (for segue: UIStoryboardSegue, sender: Any? ) hvis la redigerViewController = segue.destination som? EditViewController editViewController.modelController = modelController

Her sender vi fram forekomsten av ModelController som holder tilstanden til appen. Dette er hvor avhengighetsinjeksjonen for EditViewController skjer.

I EditViewController, Vi må oppdatere staten til det nylig innmeldte sitatet før vi går tilbake til forrige visningskontroller. Vi kan gjøre dette i en handling knyttet til Lagre knapp:

klasse EditViewController: UIViewController @IBOutlet weak var textView: UITextView! @IBOutlet svak var textField: UITextField! var modelController: ModelController! overstyr func viewDidLoad () super.viewDidLoad () la quote = modelController.quote textView.text = quote.text textField.text = quote.author @IBAction func lagre (_ sender: AnyObject) la newQuote = Sitat (tekst: textView.text, forfatter: textField.text!) modelController.quote = newQuote avvis (animert: true, fullføring: null)

Initialiser modellkontrolleren

Vi er nesten ferdige, men du har kanskje lagt merke til at vi fortsatt mangler noe: QuoteViewController passerer ModelController til EditViewController gjennom avhengighetsinjeksjon. Men hvem gir denne forekomsten til QuoteViewController i utgangspunktet? Husk at når man bruker avhengighetsinjeksjon, bør en visningsregulator ikke skape sine egne avhengigheter. Disse må komme fra utsiden.

Men det er ingen visningskontrollør før QuoteViewController, fordi dette er den første visningskontrollen til appen vår. Vi trenger noe annet objekt å opprette ModelController forekomst og å sende det til QuoteViewController.

Dette objektet er AppDelegate. Appendiatets rolle er å svare på appens livssyklusmetoder og konfigurere appen tilsvarende. En av disse metodene er applikasjons (_: didFinishLaunchingWithOptions :), som blir kalt så snart appen starter. Det er her vi lager forekomsten av ModelController og send det til QuoteViewController:

klasse AppDelegate: UIResponder, UIApplicationDelegate var vindu: UIWindow? func program (_ søknad: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool hvis la quoteViewController = window? .rootViewController som? QuoteViewController quoteViewController.modelController = ModelController () return true

Vår app er nå fullført. Hver visningskontroller får tilgang til den globale tilstanden til appen, men vi bruker ikke singletoner hvor som helst i vår kode.

Du kan laste ned Xcode prosjektet for dette eksempelet app i opplæringen GitHub repo.

konklusjoner

I denne artikkelen har du sett hvordan bruk av singletoner for å formidle staten i en iOS-app er en dårlig praksis. Singletons skaper mange problemer, til tross for at det er veldig enkelt å lage og bruke.

Vi løste problemet ved å se nærmere på MVC-mønsteret og forstå mulighetene som er skjult i det. Gjennom bruk av modellkontrollere og avhengighetsinjeksjon kunne vi forplante tilstanden til appen over alle visningskontrollere uten å bruke singletoner.

Dette er et enkelt eksempelapp, men konseptet kan generaliseres til programmer av noe kompleksitet. Dette er standard beste praksis for å formidle tilstand i iOS-apper. Jeg bruker det nå i hver app jeg skriver for mine klienter.

Noen ting du må huske på når du utvider konseptet til større apper:

  • Modellkontrolleren kan lagre tilstanden til appen, for eksempel i en fil. På denne måten blir våre data husket hver gang vi lukker appen. Du kan også bruke en mer kompleks lagringsløsning, for eksempel Core Data. Min anbefaling er å beholde denne funksjonaliteten i en separat modellkontroll som bare tar vare på lagring. Den kontrolleren kan da brukes av modellkontrolleren som holder tilstanden til appen.
  • I en app med en mer komplisert strøm, vil du ha mange beholdere i appflowen din. Disse er vanligvis navigasjonskontrollere, med sporadisk fanestyring. Begrepet avhengighetsinjeksjon gjelder fortsatt, men du må ta hensyn til beholderne. Du kan enten grave inn i deres innebygde visningskontrollere når du utfører avhengighetsinjeksjonen, eller opprette egendefinerte containerklasser som passerer modellkontrolleren på.
  • Hvis du legger til nettverk i appen din, bør dette også gå i en separat modellkontroller. En visningskontroller kan utføre en nettverksforespørsel gjennom denne nettverkskontrolleren og deretter sende de resulterende dataene til modellkontrolleren som holder staten. Husk at rollen som en visningsregulator er akkurat dette: å fungere som et limobjekt som overfører data rundt mellom objekter.

Hold deg oppdatert for flere iOS apputviklings tips og beste praksis!