Swift From Scratch Initialisering og Initializer Delegation

I den forrige leksjonen Swift From Scratch opprettet vi et funksjonelt oppgaveprogram. Datamodellen kan imidlertid bruke litt kjærlighet. I denne siste leksjonen skal vi reflektere datamodellen ved å implementere en tilpasset modellklasse.

1. Datamodellen

Datamodellen vi skal implementere inkluderer to klasser, a Oppgave klasse og a Å gjøre klasse som arver fra Oppgave klasse. Mens vi lager og implementerer disse modellklassene, fortsetter vi utforskningen av objektorientert programmering i Swift. I denne leksjonen vil vi zoome inn på initialiseringen av klassetilfeller og hvilken rolle arv spiller under initialiseringen.

De Oppgave Klasse

La oss starte med implementeringen av Oppgave klasse. Opprett en ny Swift-fil ved å velge Ny> Fil ... fra Xcode er Fil Meny. Velge Swift File fra iOS> Kilde seksjon. Gi filen navnet Task.swift og treffer Skape.

Den grunnleggende implementeringen er kort og enkel. De Oppgave Klassen arver fra NSObject, definert i Fundament rammeverk, og har en variabel eiendom Navn av type string. Klassen definerer to initiativer, i det() og init (navn :). Det er noen detaljer som kan gi deg opp, så la meg forklare hva som skjer.

importere Foundation Class Oppgave: NSObject var navn: String bekvemmelighet overstyre init () self.init (navn: "Ny oppgave") init (navn: String) self.name = name

Fordi det i det() Metoden er også definert i NSObject klasse, må vi prefikse initialisereren med overstyring søkeord. Vi dekket overordnede metoder tidligere i denne serien. I i det() Metode, vi påberoper init (navn :) metode, passerer inn "Ny oppgave" som verdien for Navn parameter.

De init (navn :) metoden er en annen initialiserer, aksepterer en enkelt parameter Navn av type string. I denne initialiseringen, verdien av Navn parameteren er tilordnet til Navn eiendom. Dette er lett nok til å forstå. Ikke sant?

Utpekte og bekvemmelighetsinitiatorer

Hva er med bekvemmelighet søkeord som prefikserer i det() metode? Klasser kan ha to typer initiatorer, utpekt initiativer og bekvemmelighet initializers. Convenience initializers er prefixed med bekvemmelighet søkeord, noe som innebærer det init (navn :) er en utpekt initialiserer. Hvorfor det? Hva er forskjellen mellom utpekte og bekvemmelighetsinitiatorer?

Utpekte initiativer fullstendig initialisere en forekomst av en klasse, noe som betyr at hver egenskap av forekomsten har en startverdi etter initialisering. Ser på Oppgave klasse, for eksempel ser vi at Navn Eiendommen er satt med verdien av Navn parameteren til init (navn :) initializer. Resultatet etter initialisering er en fullt initialisert Oppgave forekomst.

Praktiske initiativer, imidlertid stole på en utpekt initialiserer for å lage en fullt initialisert forekomst av klassen. Det er derfor i det() initialiserer av Oppgave klassen påkaller init (navn :) initialiserer i implementeringen. Dette kalles initieringsdelegasjon. De i det() initialiserer initierer initialisering til en utpekt initialiserer for å opprette en fullt initialisert forekomst av Oppgave klasse.

Praktiske initiativer er valgfrie. Ikke hver klasse har en bekvemmelighetsinitiator. Utpekte initiativer er påkrevd, og en klasse må ha minst en utpekt initialiserer for å lage en fullt initialisert forekomst av seg selv.

De NSCoding protokoll

Gjennomføringen av Oppgave klassen er ikke fullført, skjønt. Senere i denne leksjonen skriver vi en rekke Å gjøre forekommer til disk. Dette er bare mulig hvis forekomster av Å gjøre klassen kan kodes og dekodes.

Ikke vær redd, skjønt, dette er ikke rakettvitenskap. Vi trenger bare å lage Oppgave og Å gjøre klasser samsvarer med NSCoding protokoll. Det er derfor Oppgave Klassen arver fra NSObject klasse siden NSCoding protokollen kan bare implementeres av klasser som arver direkte eller indirekte fra NSObject. Som NSObject klasse, den NSCoding protokollen er definert i Fundament rammeverk.

Å vedta en protokoll er noe vi allerede har dekket i denne serien, men det er noen få gotker som jeg vil påpeke. La oss begynne med å fortelle kompilatoren at Oppgave klassen stemmer overens med NSCoding protokollen.

importere grunnlagsklasse Oppgave: NSObject, NSCoding var navn: String ...

Deretter må vi implementere de to metodene deklarert i NSCoding protokollen, init? (koder :) og kode (med :). Gjennomføringen er grei hvis du er kjent med NSCoding protokollen.

Import Foundation Class Oppgave: NSObject, NSCoding var navn: String @objc required init? (koder aDecoder: NSCoder) name = aDecoder.decodeObject (forKey: "navn") som! String @objc func kode (med aCoder: NSCoder) aCoder.encode (navn, forKey: "navn") bekvemmelighet overstyre init () self.init (navn: "Ny oppgave") init (navn: String) self.name = name

De init? (koder :) initialiserer er en utpekt initialiserer som initialiserer a Oppgave forekomst. Selv om vi implementerer init? (koder :) metode for å tilpasse seg NSCoding protokoll, trenger du aldri å påkalle denne metoden direkte. Det samme gjelder for kode (med :), som koder for en forekomst av Oppgave klasse.

De nødvendig søkeord som prefikserer init? (koder :) Metode indikerer at hver underklasse av Oppgave klassen må implementere denne metoden. De nødvendig Søkeordet gjelder bare for initiativer, og derfor trenger vi ikke å legge det til i kode (med :) metode.

Før vi går videre, må vi snakke om @objc Egenskap. Fordi det NSCoding protokollen er en mål-C-protokoll, protokoll overensstemmelse kan bare kontrolleres ved å legge til @objc Egenskap. I Swift er det ikke slikt som protokollkonformitet eller valgfrie protokollmetoder. Med andre ord, hvis en klasse overholder en bestemt protokoll, verifiserer kompilatoren og forventer at alle metoder i protokollen er implementert.

De Å gjøre Klasse

Med Oppgave klasse implementert, er det på tide å implementere Å gjøre klasse. Opprett en ny Swift-fil og gi den navnet ToDo.swift. La oss se på implementeringen av Å gjøre klasse.

importere Foundation class ToDo: Oppgave var ferdig: Bool @objc kreves init? (koder aDecoder: NSCoder) self.done = aDecoder.decodeBool (forKey: "ferdig") super.init (koder: aDecoder) @objc overstyr func kode (med aCoder: NSCoder) aCoder.encode (ferdig, forKey: "ferdig") super.encode (med: aCoder) init (navn: String, ferdig: Bool) self.done = ferdig super.init : Navn)  

De Å gjøre Klassen arver fra Oppgave klasse og erklærer en variabel egenskap ferdig av type bool. I tillegg til de to nødvendige metodene til NSCoding protokoll som den arver fra Oppgave klassen, erklærer den også en utpekt initialiserer, init (navn: gjort :).

Som i mål-C, super søkeord refererer til superklassen, den Oppgave klasse i dette eksemplet. Det er en viktig detalj som fortjener oppmerksomhet. Før du påkaller init (navn :) metode på superklassen, hver eiendom deklarert av Å gjøre klassen må initialiseres. Med andre ord, før Å gjøre klasse delegater initialisering til sin superklasse, hver eiendom definert av Å gjøre klassen må ha en gyldig startverdi. Du kan bekrefte dette ved å bytte rekkefølgen på setningene og inspisere feilen som dukker opp.

Det samme gjelder for init? (koder :) metode. Vi initialiserer først ferdig eiendom før påkalling init? (koder :) på superklassen.

Initialiserer og arv

Når det gjelder arv og initialisering, er det noen regler å huske på. Regelen for utpekte initiativer er enkel.

  • En utpekt initialiserer må påkalle en utpekt initialiserer fra sin superklasse. I Å gjøre klasse, for eksempel, den init? (koder :) metode påberoper seg init? (koder :) metode for superklassen. Dette kalles også delegere opp.

Reglene for bekvemmelighetsinitiatorer er litt mer komplekse. Det er to regler å huske på.

  • En bekvemmelighetsinitiator må alltid påkalle en annen initialiserer av klassen den er definert i. I Oppgave klasse, for eksempel, den i det() Metoden er en praktisk initialiserer og delegerer initialisering til en annen initiator, init (navn :) i eksemplet. Dette er kjent som delegere over.
  • Selv om en bekvemmelighetsinitiator ikke trenger å delegere initialisering til en utpekt initialiserer, må en bekvemmelighetsinitialiser ringe en utpekt initialiserer på et tidspunkt. Dette er nødvendig for å fullt ut initialisere forekomsten som initialiseres.

Med begge modellklasser implementert, er det på tide å refactor ViewController og AddItemViewController klasser. La oss starte med sistnevnte.

2. refactoring AddItemViewController

Trinn 1: Oppdater AddItemViewControllerDelegate protokoll

De eneste endringene vi må gjøre i AddItemViewController klassen er relatert til AddItemViewControllerDelegate protokoll. I protokolldeklarasjonen endrer du typen av didAddItem fra string til Å gjøre, modellklassen vi implementerte tidligere.

protokoll AddItemViewControllerDelegate func controller (_ kontroller: AddItemViewController, didAddItem: ToDo)

Trinn 2: Oppdater skape(_:) Handling

Dette betyr at vi også må oppdatere skape(_:) handling der vi anvender delegatemetoden. I den oppdaterte implementeringen lager vi en Å gjøre eksempel, passerer den til delegatemetoden.

@IBAction func create (_ sender: Any) hvis la navn = textField.text // Opprett element let element = ToDo (navn: navn, ferdig: false) // Varsle Delegat delegat? .Controller (selvtillit, gjordeAddItem: )

3. refactoring ViewController

Trinn 1: Oppdater elementer Eiendom

De ViewController klassen krever litt mer arbeid. Vi må først endre typen av elementer eiendom til [Å gjøre], en rekke av Å gjøre forekomster.

var elementer: [ToDo] = [] didSet (oldValue) la hasItems = items.count> 0 tableView.isHidden =! hasItems messageLabel.isHidden = hasItems

Trinn 2: Tabellvisning Datakilde Metoder

Dette betyr også at vi må refactor noen andre metoder, for eksempel Tableview (_: cellForRowAt :) metode vist nedenfor. Fordi det elementer array inneholder nå Å gjøre forekomster, sjekker om et element er merket som gjort er mye enklere. Vi bruker Swifts ternære betingede operatør for å oppdatere tabellvisningscellens tilbehørstype.

func tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell // Hente element let element = elementer [indexPath.row] // Dequeue Cell lar celle = tableView.dequeueReusableCell (withIdentifier: "TableViewCell", for: indexPath ) // Konfigurer Cell cell.textLabel? .Text = item.name cell.accessoryType = item.done? .checkmark: .none returner celle

Når brukeren sletter et element, trenger vi bare å oppdatere elementer eiendom ved å fjerne tilsvarende Å gjøre forekomst. Dette gjenspeiles i implementeringen av Tableview (_: begå: forRowAt :) metode vist nedenfor.

func tableView (_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) hvis editingStyle == .delete // Oppdater elementer items.remove (på: indexPath.row) // Oppdater tabelloversikt tableView.deleteRows : [indexPath], med: .right) // Lagre stat saveItems ()

Trinn 3: Tabellvisning Delegate Methods

Oppdaterer tilstanden til et element når brukeren tapper en rad håndteres i Tableview (_: didSelectRowAt :) metode. Gjennomføringen av dette UITableViewDelegate Metoden er mye enklere takket være Å gjøre klasse.

func tableView (_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) tableView.deselectRow (ved: indexPath, animated: true) // Hente Item let item = elementer [indexPath.row] // Oppdater element item.done =! gjort // Hent celle la celle = tableView.cellForRow (ved: indexPath) // Oppdater Cell celle? .accessoryType = item.done? .checkmark: .none // Save State saveItems ()

Den tilsvarende Å gjøre forekomsten er oppdatert, og denne endringen reflekteres av tabellvisningen. For å redde staten, påberoper vi oss saveItems () i stedet for saveCheckedItems ().

Trinn 4: Legg til produktvisning Controller Delegate Methods

Fordi vi oppdaterte AddItemViewControllerDelegate protokoll, må vi også oppdatere ViewControllerImplementering av denne protokollen. Endringen er imidlertid enkel. Vi trenger bare å oppdatere metoden signaturen.

func controller (_controller: AddItemViewController, didAddItem: ToDo) // Oppdater datakilde items.append (didAddItem) // Lagre stat saveItems () // Oppdater tabelloversikt tableView.reloadData () // Avvis Legg til produkt Vis Controller avvis animert: sant)

Trinn 5: Lagre elementer

De pathForItems () Metode

I stedet for å lagre elementene i brukerstandarddatabasen, skal vi lagre dem i programmets katalog. Før vi oppdaterer loadItems () og saveItems () metoder, vi skal implementere en hjelpemetode som heter pathForItems (). Metoden er privat og returnerer en bane, plasseringen av elementene i dokumentmappen.

privat func pathForItems () -> String guard la documentsDirectory = NSSearchPathForDirectoriesInDomains (.documentDirectory, .userDomainMask, true) .first, la url = URL (streng: documentsDirectory) ellers fatalError ("Dokumentsøk ikke funnet") returner url. appendingPathComponent ("items"). path

Vi henter først banen til dokumentkatalogen i programmets sandkasse ved å påkalle NSSearchPathForDirectoriesInDomains (_: _: _ :). Fordi denne metoden returnerer en rekke strenger, tar vi det første elementet.

Legg merke til at vi bruker en vakt uttalelse for å sikre verdien returnert av NSSearchPathForDirectoriesInDomains (_: _: _ :) er gyldig. Vi kaster en dødelig feil hvis denne operasjonen mislykkes. Dette avslutter umiddelbart søknaden. Hvorfor gjør vi dette? Hvis operativsystemet ikke kan gi oss veien til dokumentmappen, har vi større problemer å bekymre seg for.

Verdien vi kommer tilbake fra pathForItems () er sammensatt av banen til dokumentmappen med strengen "elementer" lagt til det.

De loadItems () Metode

LastItems metode endrer seg ganske mye. Vi lagrer først resultatet av pathForItems () i en konstant, sti. Vi unarchive deretter objektet arkivert på den banen og nedkastet den til et valgfritt utvalg av Å gjøre forekomster. Vi bruker valgfri binding for å pakke ut valgfri og tildele den til en konstant, elementer. I hvis klausul, tilordner vi verdien som er lagret i elementer til elementer eiendom.

private func loadItems () let path = pathForItems () hvis la ting = NSKeyedUnarchiver.unarchiveObject (withFile: path) som? [ToDo] self.items = items

De saveItems () Metode

De saveItems () Metoden er kort og enkel. Vi lagrer resultatet av pathForItems () i en konstant, sti, og påkalle archiveRootObject (_: toFile :)NSKeyedArchiver, passerer i elementer eiendom og sti. Vi skriver ut resultatet av operasjonen til konsollen.

privat func saveItems () let path = pathForItems () hvis NSKeyedArchiver.archiveRootObject (self.items, toFile: path) skriv ut ("vellykket lagret") else print ("Lagring mislyktes")

Trinn 6: Rengjør

La oss avslutte med den morsomme delen, å slette koden. Begynn med å fjerne checkedItems eiendom øverst siden vi ikke lenger trenger det. Som et resultat kan vi også fjerne loadCheckedItems () og saveCheckedItems () metoder, og enhver henvisning til disse metodene i ViewController klasse.

Bygg og kjør programmet for å se om alt fortsatt fungerer. Datamodellen gjør programmets kode mye enklere og mer pålitelig. Takk til Å gjøre klassen, administrerer elementene i vår liste mye, er nå enklere og mindre feilgodt.

Konklusjon

I denne leksjonen reflekterte vi datamodellen i søknaden vår. Du lærte mer om objektorientert programmering og arv. Instansinitialisering er et viktig konsept i Swift, så sørg for at du forstår hva vi har dekket i denne leksjonen. Du kan lese mer om initialisering og initieringsdelegasjon i The Swift Programming Language.

I mellomtiden kan du sjekke ut noen av våre andre kurs og opplæringsprogrammer om swift språk iOS-utvikling!