10 grundläggande principer för att använda CoreData utan att skjuta huvudet av dig

Jag har använt CoreData i flera projekt under min karriär som utvecklare av Apple-plattformen (första gången var i MacOS 10.5…), och genom åren har jag lärt mig saker på det hårda sättet (dvs. genom att slå huvudet mot väggen alldeles för länge). Så jag tänkte att jag skulle dela med mig av mina lärdomar till er. Förhoppningsvis sparar jag dig några blåmärken i pannan.

Notera att detta inte är en instruktionsguide för hur man ställer in CoreData. Det är tänkt att vara en artikel med lite god praxis som du kan plocka upp för dina projekt.

Här är Apples snabba översikt: ”Använd Core Data för att spara din applikations permanenta data för användning offline, för att cacha tillfälliga data och för att lägga till ångra funktionalitet i din app på en enda enhet.”

Om att ge lite mer detaljer är CoreData Apples teknik för att spara dina strukturerade data lokalt. Du använder modellredigeraren för att definiera din datastruktur precis som en databasschemaredigerare, sedan kan du använda dessa objekt i din kod för att lagra dina data.

CoreData är ett ramverk för hantering av objektgrafer

Vad detta innebär är att när du skapar din modell definierar du dina objekt och deras relationer. Om du till exempel har en enhet Company och en enhet Employee i din datamodell kommer dessa två enheter att kopplas samman av en relation som länkar dessa två. Förmodligen med namnet employees på enheten Company och employer på enheten Employee. Du lägger helt enkelt till relationsegenskapen på entiteterna och det är allt. Du behöver inte skapa en sammanfogningstabell som i en databas.

Det fina med CoreData jämfört med en traditionell databas är att när du frågar efter företag X kan du få fram de anställda direkt genom att helt enkelt komma åt company.employees. Du behöver inte bygga en join-fråga eller göra en ny fråga.

CoreData kan lagra data i olika format

Ja, du hörde rätt, CoreData kan spara dina data i olika format/på olika ställen. Var och en har sina för- och nackdelar.

  1. Binary store
  2. SQLite Store
  3. In Memory store

Du kan läsa den här artikeln för att få reda på för- och nackdelarna med varje. Jag använder alltid SQLite, men jag kan se hur en minneslagring skulle vara supersnabb.

Ett par begreppsdefinitioner

NSManagedObject: Detta är basklassen som representerar de objekt som du har lagrat i CoreData.

Fetch request: Detta är motsvarigheten till en förfrågan i CoreDatas värld. Den representeras av klassen NSFetchedObject.

Använd NSPersistentContainer

Förutom iOS 10.0 innebar inställningen av CoreData-stacken en hel del boilerplate-kod. Du var tvungen att själv definiera vilken strategi du skulle använda för att hantera flertrådig användning av CoreData.

Med NSPersistentContainer kapslar Apple in det mesta av koden. Det besparar dig debatten om hur du bäst ställer in din stack för optimal prestanda.

Denna artikel innehåller all information du behöver för att använda den på rätt sätt.

Använd relationer

Jag nämner alltid detta eftersom CoreData-modellen vid två tillfällen, för projekt som jag ärvde, ställdes upp som en databas. Inga relationer definierades. Istället lagrades ID:et för det relaterade objektet och därför krävdes en extra hämtningsförfrågan för att hämta det relaterade objektet. Jag tror att många utvecklare närmar sig CoreData som en databas och begår detta fel. Det slutar med att hela projektet blir mycket mer komplext och mycket långsammare.

Följ den här artikeln för att komma igång.

Undvik stringly-kod så mycket som möjligt

Stringly-kod hänvisar till kod som använder strängar för att komma åt data. På iOS har du ett par stringly API:er: UserDefaults, CoreData är några som jag kommer att tänka på.

Om vi återgår till företagsexemplet finns det nedan två alternativ för att skapa ett företag och ange dess namn:

//stringly version to create and edit a company
let company = NSEntityDescription.insertNewObject(forEntityName: "Company", in: managedObjectContext)
company.set("my less than great corporation", forKey: "name")//non stringly version
let company = Company(in: managedObjectContext)
company.name = "My super corporation"

Båda alternativen fungerar vid en första anblick, de kompileras, det är inget fel med dessa…

Problemet med sträng kod är att kompilatorn inte kan hjälpa dig. Tänk dig att du behöver komma åt name av företaget på flera ställen i koden, och realistiskt sett behöver du minst två: en för att ställa in den och en för att läsa den, du måste nu skriva den name strängen på två ställen. Visst kan du skapa en variabel för att lagra strängen och använda den variabeln, men det kräver extra ansträngning, det kommer inte att skrivas kontrollerat och vi vet alla att utvecklare är lata…

Så det lämnar vanligtvis strängen på flera ställen. När du sedan vill byta namn på den måste du leta efter den med sökning och ersättning… Om du missar en enda gång kommer din app att krascha.

Här är listan över kompilerfunktioner som du inte kommer att dra nytta av med stringly-syntaxen:

  • auto-complete
  • refactor
  • compilervarning vid felstavning
  • compilerfel om du tar bort/omdöpte den här enheten eller egenskapen i din modell

För att kunna använda den icke-stringly-versionen av koden måste du helt enkelt be kompilatorn att generera klasser för dina enheter (du kan följa Apples instruktioner under ”Välj ett alternativ för kodgenerering”). Detta är nu standard när du skapar en ny Entity.

Håll din NSPersitentContainer.viewContext skrivskyddad

För prestandaskäl vill du inte utföra långa operationer på viewContext eftersom det kommer att köras på huvudtråden och potentiellt blockera användargränssnittet.

Lösningen är att du skaffar dig en vana att endast göra ändringar av dina objekt inom ett block som du schemalägger med metoden NSPersistentContainer.performBackgroundTask(). När du sparar dina ändringar i det blocket kommer dessa att slås ihop tillbaka till NSPersitentContainer.viewContext och du kan uppdatera ditt användargränssnitt.

Bevaka dessa trådar

En av de största hindren med CoreData är att använda den på ett trådsäkert sätt. Fast när du väl har förstått konceptet är det ganska enkelt.

Vad det går ut på är att alla dataobjekt som du får från CoreData endast kan användas på den tråd som objektets managedobjectContext är kopplad till.

Ett sätt att se till att du använder rätt tråd för att komma åt ditt objekt är att använda NSManagedObjectContext.performBlockAndWait som ser till att din kod används på rätt tråd.

När du väl är i det blocket måste du använda NSManagedObject.objectID och NSManagedObjectContext.existingObject(withId:) för att skicka objektet över trådar och kontext.

Om du inte gör det här kommer du med största sannolikhet att få deadlocks. Tänk dig:

  • Du schemalägger en uppgift från huvudtråden med I det fall då något schemaläggs från huvudtråden NSManagedObjectContext.performBlockAndWait(). Vid denna tidpunkt är huvudtråden blockerad och väntar på att detta block ska slutföras.
  • Inom detta block skickar du objektet foo från huvudkontexten till metoden bar() och skickar det till en annan tråd.
  • För att vara en god CoreData-medborgare hämtar bar() kontexten som är kopplad till foo och skickar sin uppgift med hjälp av foo.managedObjectContext.performBlockAndWait()
  • Boom, du är låst eftersom den här kontexten bara körs på huvudtråden och huvudtråden väntar på att detta ska slutföras.

En annan konvention som hjälper är att skicka objekt mellan view controllers endast om de finns i huvudtrådskontexten.

Använd launch-argumentet för att spåra kontext och trådning

Om du följer tipsen ovan bör detta begränsa chansen att en NSManagedObject-instans anropas på fel tråd. Men detta kan fortfarande hända. Så se till att när du är i utveckling att slå på trådfelsökningsförsäkran

-com.apple.CoreData.ConcurrencyDebug 1

Denna artikel berättar exakt hur du gör det och berättar om andra flaggor som du kan ställa in för mer felsökning.

Använd aggregerade sökningar

CoreData erbjuder groupBy, sum och count som aggregerade funktioner som kan köras under en sökning. Det kan vara lite komplicerat att konfigurera den, men det är väl värt det.

Alternativet är att skapa en fråga för att få fram alla objekt som matchar dina kriterier och sedan köra en for-slinga genom alla enheter och göra beräkningen själv. Lättare att koda, men mycket mindre effektivt.

När du kör en begäran om aggregerad hämtning översätter CoreData den till en databasfråga och kör beräkningen direkt på databasnivå. Det gör det supersnabbt.

Denna artikel är en bra introduktion till hur man gör det.

Håll CoreData-relaterad kod i kategorier

När du har din NSManagedObject-underklass skapad av Xcode kommer du troligen att upptäcka att du har kod som bör finnas i klassen som representerar ditt objekt (för att beräkna värden, importera data från servern, etc). Men du har bett CoreData att skapa klasserna åt dig…

Så du måste skapa en kategori för det objekt som behöver extra beteende. Lägg bara kod som är relaterad till CoreData där så att du kan hålla det rent.

Anta en transaktionsmodell för dina ändringar av dina data

Du kan ändra dina objekt hur mycket som helst, men för att persistera dessa ändringar måste du anropa NSManagedObjectContext.save().

Du kommer snabbt att glömma att anropa save() och du kommer att ha några hängande ändringar i ditt sammanhang. I det här läget är allting okej, nästa gång du anropar save() kommer du inte att vara säker på vad du bekräftar.

Anta en transaktionsmodell för att undvika att lämna en smutsig kontext efter dig. För att göra det skapar du en kategori på NSPersistentContainer så här:

extension NSPersistentContainer {
@objc func performBackgroundSaveTask(_ block: @escaping (NSManagedObjectContext) -> Void) {
performBackgroundTask { (context: NSManagedObjectContext) in
block(context)
context.save()
}
}
}

Säkerställ från och med nu att alla ändringar du gör exekveras i ett block schemalagt med performBackgroundSaveTask(). På så sätt är du garanterad att ändringarna sparas när du gör dem.

Spara inte kontexten från en metod i en modellkategori

Kalla aldrig save() från en enhetskategori. Det finns inget värre än att kalla en metod på ditt objekt utan att inse att den kommer att spara hela kontexten med den. Att begå några ändringar som du kanske inte vill spara. Att kalla save bör göras vid den punkt där ändringarna i objektet påbörjas.