12. Februar 2018

Objektpersistenz mit Core Data

Core Data ist ein Framework für die persistente Speicherung von Objekten gemäß eines Datenmodells. Dabei werden bidirektionale 1:n und n:m Beziehungen zwischen Objekten unterstützt. Das Datenmodell wird dabei in einer .xcdatamodel-Datei im App-Projekt abgelegt:

Xcode Schema Editor für Core Data

Core Data Klassen

Für die Verwendung von Core Data werden folgende Objekte benötigt:

Beispielcode für die Konfiguration dieser Objekte finden Sie, wenn Sie ein neues Xcode-Projekt mit File » New Project » Empty Application erstellen und die Option „Use Core Data“ aktivieren.

Entitätsklassen für Core Data

Mit „Editor » Create NSManagedObject subclass“ können in Xcode aus dem Datenmodell Swift-Klassen generiert werden. Diese erben von NSManagedObject und deklarieren ihre Eigenschaften als @NSManaged. Da der Name der Entität im Code häufig benötigt wird, empfiehlt es sich, dafür eine Konstante anzulegen:

import CoreData

class Deck : NSManagedObject {

    static let entityName = "Deck"
    
    @NSManaged var name : String?

}

Objekte anlegen

Core-Data-Objekte dürfen nicht direkt konstruiert werden, sondern müssen über die NSEntityDescription erstellt werden und so dem NSManagedObjectContext bekannt gemacht werden:

let person = NSEntityDescription.insertNewObjectForEntityForName(Deck.entityName,
                  inManagedObjectContext:managedObjectContext)

Objekte abfragen

Abfragen werden mit einem NSFetchRequest gestellt. Hier können mit den Eigenschaften predicate und sortDescriptors Filterbedingungen und Sortierreihenfolge angegeben werden:

let fetchRequest = NSFetchRequest(entityName: Deck.entityName)
fetchRequest.predicate = NSPredicate(format: "name == %@", name)
fetchRequest.sortDescriptors = [ NSSortDescriptor(key:"name", ascending:true) ]

let results = try! managedObjectContext.executeFetchRequest(fetchRequest)

Objekte löschen

Objekte werden über den NSManagedObjectContext gelöscht:

managedObjectContext.deleteObject(entity)

Schema-Migration

Core Data: Verschiedene Modell-Versionen Für Core Data-Modelldefinitionen können mehrere Versionen gleichzeitig im Projekt konfiguriert werden, mit Editor » Add Model Version werden neue Versionen hinzugefügt.

Wird der Persistent Store mit folgenden Optionen konfiguriert, werden die Modell-Versionen automatisch verglichen und daraus Schritte zur Migration der Datenbank auf die aktuelle Version abgeleitet:

let options = [
    NSMigratePersistentStoresAutomaticallyOption: true,
    NSInferMappingModelAutomaticallyOption: true
]

Dabei werden hinzugefügte und gelöschte Felder automatisch behandelt. Für umbenannte Felder kann der alte Name als Renaming ID eingetragen werden:

Core Data: Felder umbenennen

Tutorial 1: Persistenz mit Core Data implementieren

  1. Laden Sie den Start-Stand von dem Flashcards-Beispielprojekt: Flashcards.zip.

  2. Erstellen Sie unter Model mit File » New » iOS » Core Data » Data Model ein Datenmodell Flashcards.xcdatamodeld:

    Xcode: Create New Data Model
  3. Öffnen Sie das Datenmodell und fügen Sie mit Add Entity die Entität Card hinzu. Legen Sie für Card die Eigenschaften frontText und backText an und wählen Sie als Typ String aus:

    Core Data: Add Entity
  4. Im Folgenden werden im Projekt Klassen für Card generiert. Seit Xcode 8.1 generiert Xcode die Klassen jedoch zusätzlich automatisch beim Build. Deaktivieren Sie dies, indem Sie die Entität Card auswählen und Manual/None für Codegen auswählen.

    Core Data Code-Generierung deaktivieren
  5. Wählen Sie „Editor » Create NSManagedObject subclass“, um Swift-Klassen für das erstellte Datenmodell zu generieren:

    Xcode: Create NSManagedObject Subclass

    Wählen Sie das Data Model Flashcards und die Entität Card aus und konfigurieren Sie im Dateiauswahldialog, dass Swift-Klassen in der Model-Gruppe generiert werden sollen:

    Swift-Klassen generieren
  6. Löschen Sie die alte Modellklasse Card.swift.

  7. Deklarieren Sie in Card+CoreDataClass.swift eine Konstante entityName für den Namen der Core-Data-Entität:

    public class Card: NSManagedObject {
    
        static let entityName = "Card"
    
    }
  8. Fügen Sie dem Xcode-Projekt unter Helper die Extension NSPersistentContainer+Defaults.swift hinzu. Diese vereinfacht das Erstellen der für Core Data benötigten Objekte.

  9. Erstellen Sie eine Klasse FlashcardsModel, die ein Core-Data-managedObjectContext für die App erzeugt:

    import Foundation
    import CoreData
    
    class FlashcardsModel {
    
        private let persistentContainer : NSPersistentContainer
    
        init() {
            self.persistentContainer = NSPersistentContainer(defaultContainerWithName: "Flashcards")
        }
        
        var viewContext : NSManagedObjectContext {
            return self.persistentContainer.viewContext
        }
    
    }
  10. Deklarieren Sie eine berechnete in FlashcardsModel die alle Lernkarten mit einem NSFetchRequest lädt:

    var cards : [Card] {
        let request : NSFetchRequest<Card> = Card.fetchRequest()
        return try! viewContext.fetch(request)
    }
  11. Erstellen Sie eine Methode createCard in FlashcardsModel, die ein Card-Objekt mittels NSEntityDescription.insertNewObjectForEntityForName in dem managedObjectContext erzeugt:

    func createCard() -> Card {
        let card = NSEntityDescription.insertNewObject(forEntityName: Card.entityName,
                       into: self.viewContext) as! Card
        return card
    }
  12. Deklarieren Sie im FlashcardsModel eine Methode save, die die Persistierung der Änderungen an dem managedObjectContext auslöst:

    func save() {
        assert(Thread.isMainThread)
        do {
            try self.viewContext.save()
        }
        catch let error {
            NSLog("%@", "An error occurred when saving: \(error)")
        }
    }
  13. Passen Sie den StartViewController so an, dass beim Download von den Lernkarten über das FlashcardsModel Card-Entitäten angelegt werden:

    class StartViewController: UIViewController, DownloadsTableViewControllerDelegate {
    
        // ...
    
        // MARK: - DownloadsTableViewControllerDelegate
    
        func downloadFinished(terms: [Term]) {
            for term in terms {
                let card = FlashcardsModel.shared.createCard()
                card.frontText = term.term
                card.backText = term.definition
            }
            FlashcardsModel.shared.save()
            updateView()
        }
    
    }
  14. Starten Sie die App und prüfen Sie, dass in die lokale Bibliothek übernommene Kartenstapel auch nach einem Neustart der App vorhanden sind.

  15. Aktivieren Sie die automatische Code-Generierung, indem Sie in Flashcards.xcdatamodeld die Eigenschaft Codegen für die Card-Entität auf Category/Extension einstellen. Löschen Sie die im Projekt generierten Datei Card+CoreDataProperties.swift - diese werden nun automatisch beim Build erzeugt.

  16. Setzen Sie die App in den Ursprungszustand zurück, indem Sie im Simulator iOS Simulator » Reset Content and Settings wählen oder mit Hardware » Home und langem Tap auf das App-Icon die App vom Simulator löschen.

  17. Committen Sie die Änderungen: „20.1. Objektpersistenz mit Core Data“.

Tutorial 2: Core Data SQLite-Datenbank inspizieren

  1. Führen Sie die App im Simulator aus und kopieren Sie den Pfad zur sqlite3-Datenbankdatei, der in der Console ausgegeben wird:

    Pfad zur sqlite3 Datenbank

    und verwenden Sie das Terminal, um die Datenbank mit dem sqlite3-Kommando zu öffnen:

    sqlite3 /Users/bob/Library/Developer/CoreSimulator/.../Flashcards.db

    Verwenden Sie folgende Kommandos um die Datenbankinhalte anzuzeigen:

    sqlite> .help
    ...
    
    sqlite> .tables
    ZCARD         Z_METADATA    Z_MODELCACHE  Z_PRIMARYKEY
    
    sqlite> .mode column
    sqlite> .headers on
    
    sqlite> select * from ZCARD;
    
    Z_PK        Z_ENT       Z_OPT       ZBACKTEXT   ZFRONTTEXT
    ----------  ----------  ----------  ----------  ----------
    1           1           1           Vogel       bird
    2           1           1           Baum        tree
    3           1           1           Haus        house
  2. Beenden Sie sqlite3 mit .quit.

Tutorial 3: Migration bei Schema-Änderungen

  1. Öffnen Sie Flashcards.xcdatamodeld und wählen Sie im Menü Editor » Add Model Version... um eine Modell-Version Flashcards 2 zu erstellen:

    Core Data: Model-Version hinzufügen

  2. Fügen Sie Card ein Attribut scheduleDate vom Typ Date hinzu:

  3. Wählen Sie Flashcards 2 als aktive Model Version aus:

    Core Data Model Version auswählen
  4. Starten Sie die App testweise - die bestehende Datenbank wird automatisch migriert.

  5. Committen Sie die Änderungen als „20.3. Card.scheduleDate per Model Version hinzugefügt“.