Bilder via Core Data speichern – Teil 2

Speichern von Bildern im Dateisystem

Ergänzend zum ersten Teil dieser Reihe zeige ich euch in diesem Artikel, wie ihr Bilder im Dateisystem einer iOS-App speichern und sie über Core Data-Entities referenzieren könnt. Hierbei gehe ich ausschließlich auf die notwendigen Änderungen ein und stelle am Ende beide Verfahren und ihre Vor- und Nachteile gegenüber.

Schritt 1: Erstellen eines File-System-Helpers

Wie geschrieben sollen die Bilder nun im Dateisystem abgelegt und daraus geladen werden. Zu diesem Zweck binde ich in das Projekt zunächst einen neuen Helper-Typ namens ImageFileHandler ein. Der besitzt eine Typ-Methode, die als Parameter einen Dateinamen erweitern. Als Ergebnis liefert die Methode eine URL zurück, die auf jene Datei im JPG-Format innerhalb des Dokumentenverzeichnisses verweist.

struct ImageFileHandler {
    static func fileURL(for fileName: String) -> URL {
        let documentDirectory = try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
        return documentDirectory.appendingPathComponent("\(fileName).jpg")
    }
}

Diese Methode können wir nutzen, um sowohl Bilder zu speichern als auch wieder aus dem Dateisystem auszulesen.

Schritt 2: Aktualisierung von SavedImage

Die SavedImage-Klasse soll nicht länger die Bild-Daten enthalten. Aus diesem Grund entfernen wir aus dem Core Data-Modell die data-Eigenschaft. Stattdessen kommt eine neue Eigenschaft namens uuid vom gleichnamigen Typ dazu. Jede SavedImage-Instanz soll eine solche eindeutige UUID bei der Erstellung erhalten. Der Name des zugehörigen Bildes im Dateisystem soll ebenfalls jener UUID entsprechen, um so automatisch eine Verknüpfung zwischen SavedImage-Instanz und Bild im Dateisystem herzustellen.

Das bisherige data-Attribut wird durch eine neue uuid-Eigenschaft ergänzt.
Das bisherige data-Attribut wird durch eine neue uuid-Eigenschaft ergänzt.

Schritt 3: Aktualisierung des CoreDataManager

Zum Erstellen und Speichern der Bilder soll weiterhin die createSavedImage(with:)-Methode des CoreDataManager zum Einsatz kommen. Statt die als Parameter übergebenen Daten direkt in der neuen SavedImage-Instanz zu speichern, müssen die Daten stattdessen im Dateisystem abgelegt werden. Zu diesem Zweck kommt eine neue Hilfsmethode namens saveImageInFileSystem(imageData:fileName:) zum Einsatz. Als Dateiname wird die neue UUID genutzt, die eine SavedImage-Instanz nun anstelle des Daten-BLOBS erhält. Der nachfolgende Code zeigt die entsprechende Aktualisierung des CoreDataManager-Codes (die Änderungen beziehen sich nur auf die Extension, die sonstige Implementierung bleibt unberührt).

extension CoreDataManager {
    @discardableResult func createSavedImage(with data: Data) -> SavedImage {
        let savedImage = SavedImage(context: viewContext)
        savedImage.uuid = UUID()
        savedImage.creationDate = Date()
        try? saveImageInFileSystem(imageData: data, fileName: savedImage.uuid!.uuidString)
        saveContext()
        return savedImage
    }
    
    func saveImageInFileSystem(imageData: Data, fileName: String) throws {
        let fileURL = ImageFileHandler.fileURL(for: fileName)
        try imageData.write(to: fileURL)
    }
}

Mit diesen Änderungen werden bereits erfolgreich Bilder im Dateisystem gespeichert und lassen sich über die UUID eines SavedImage referenzieren.

Schritt 4: Erweiterung von SavedImage um data-Eigenschaft

Abschließend ergänzen wir noch den umgekehrten Weg, über den sich aus einer SavedImage-Instanz heraus auf die Daten des zugehörigen Bildes zugreifen lässt. Zu diesem Zweck erzeugen wir eine Extension für SavedImage und implementieren eine Computed Property namens data vom Typ Data?. Die liest die zugehörigen Bilddaten über die passende File-URL aus und liefert sie als Ergebnis zurück.

extension SavedImage {
    var data: Data? {
        guard let uuid = self.uuid else {
            return nil
        }
        let fileURL = ImageFileHandler.fileURL(for: uuid.uuidString)
        return try? Data(contentsOf: fileURL)
    }
}

Schritt 5: Das war’s!

Und damit sind wir auch schon durch mit den Änderungen, weitere Anpassungen am bestehenden Code sind nicht notwendig! Die Bilder werden nun im Dateisystem abgelegt und daraus ausgelesen. Die SavedImage-Instanzen dienen lediglich als Referenz auf die verschiedenen Bilder.

Frage: Speicherung über Core Data oder im File-System?

Generell empfiehlt es sich, Bilddaten und sonstige binäre Daten wie Audio-Files oder Videos im Dateisystem statt in einer Datenbank zu speichern. Hierfür gibt es verschiedene Gründe. So kann die Datenbank-Performance leiden, wenn sich zu viele Daten-BLOBs ansammeln. Außerdem ist das Auslesen von Daten-BLOBs aus einer Datenbank mitunter aufwendiger als der Zugriff auf das Dateisystem.

Handelt es sich jedoch nur um einzelne Binärdaten die im Idealfall auch nicht besonders groß sind, ist es in meinen Augen vertretbar, diese direkt in der Datenbank abzulegen. In einem meiner Projekte beispielsweise wird initial eine Logo-Datei geladen. Die ist nicht allzu groß und es kommen auch keine weiteren Bilder hinzu, weshalb ich den zugehörigen Daten-BLOB in diesem Fall direkt in der Datenbank gespeichert habe.

Oftmals ist das direkte Speichern von Binärdaten in Datenbanken schlicht Komfort für uns Entwickler, da wir so über die zugehörige Instanz direkt auf jene Binärdaten zugreifen können. Im Falle der Bilder hatten wir die jeweiligen Daten zusammen mit der SavedImage-Instanz direkt im Zugriff. Bei Einsatz des File-Systems müssen wir die Daten erst auslesen.

Auch beim Aktualisieren oder Löschen ist Vorsicht geboten. Löscht man beispielsweise eine SavedImage-Instanz und speichert die Bilder im Dateisystem, muss man sicherstellen, auch das zugehörige Bild aus dem Dateisystem zu entfernen. Solche Vorgänge sind nicht nötig, wenn die Daten direkt mit der SavedImage-Instanz verknüpft sind; die Daten werden dann direkt zusammen mit der Instanz entfernt.

Weiterführende Links zum Artikel


Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert