Bilder via Core Data speichern – Teil 1

Umwandlung von Bildern in Data-Instanzen

Eine häufige Anfrage aus der Community bezieht sich auf das Speichern und Auslesen von Bildern mittels Core Data. Aus diesem Grund möchte ich in dieser kleinen Artikelreihe die in meinen Augen zwei typischen Vorgehensweisen erläutern, über die sich Bilder mit Hilfe von Core Data abspeichern und wieder abrufen lassen.

Eines vorneweg: Bei diesem Thema geht es nicht ausschließlich um Core Data. Ja, Core Data kommt zum Einsatz, um Bilddaten oder Verweise auf Bilddateien zu speichern. Die Umwandlung eines Bildes in eine Data-Instanz und wieder zurück bzw. den Zugriff auf das Dateisystem muss man aber unabhängig von Core Data steuern. Viele Elemente in diesem und den nachfolgenden Artikeln haben also nicht primär Core Data als Schwerpunkt, doch das Framework kommt zum Einsatz, um auf persistent gespeicherte Bilder zugreifen zu können.

Als Ausgangspunkt für den Beispiel-Code in diesem Artikel kommt das TS ImagePickerView Example-Projekt zum Einsatz. Es enthält Funktionen, um auf die Foto-Library zugreifen zu können. Dieses Konzept wird im Folgenden erweitert bzw. abgewandelt, um über die Foto-Library ausgewählte Bilder persistent mithilfe von Core Data abzuspeichern.

Speicherung als Data-Instanz

Dieser Artikel befasst sich mit der Speicherung von Bildern direkt innerhalb einer Entität von Core Data. Dazu kommt ein Attribut auf Basis des Binary Data-Typs zum Einsatz. Diese Eigenschaft erhält die Daten eines Bildes, was zu einer dauerhaften Speicherung jener Daten über Core Data führt. Ein zweiter Artikel betrachtet zu einem späteren Zeitpunkt die Speicherung eines Bildes über das Dateisystem.

Grundlage: Das Datenmodell

Wie bei der Arbeit mit Core Data üblich, benötigen wir zunächst ein Datenmodell. Jenes Datenmodell halte ich bewusst simpel. Es trägt den Namen ImagePickerDataModel und verfügt über lediglich eine einzige Entität namens SavedImage. Jedes Bild, das persistent gespeichert werden soll, wird als eine Instanz vom Typ SavedImage abgebildet.

Die Entität SavedImage besitzt zwei Eigenschaften: data ist vom Typ Binary Data und dient zur Speicherung der Bilddaten. creationDate ist eine Hilfseigenschaft vom Typ Date, über die das Erstellungsdatum einer SavedImage-Instanz hinterlegt wird. Letzteres dient zur Optimierung der Sortierung der gespeicherten Bilder, wenn diese via Grid (dazu kommen wir noch) nacheinander aufgeführt werden.

Das Datenmodell beschränkt sich für dieses Beispiel auf das nötigste.
Das Datenmodell beschränkt sich für dieses Beispiel auf das nötigste.

Umsetzung eines CoreDataManager

Um unser Datenmodell aus dem Code heraus nutzen zu können, implementiere ich einen Typen namens CoreDataManager. Der baut den Core Data-Stack auf und stellt über eine Extension eine Methode namens createSavedImage(with:) zur Verfügung. Diese Methode dient zum Erstellen und Speichern einer neuen SavedImage-Instanz auf Basis der übergebenen Daten.

import CoreData
import Foundation

@MainActor
struct CoreDataManager {
    static let shared = CoreDataManager()
    
    let container: NSPersistentContainer
    
    var viewContext: NSManagedObjectContext {
        container.viewContext
    }
    
    init() {
        container = NSPersistentContainer(name: "ImagePickerDataModel")
        container.loadPersistentStores(completionHandler: { (_, _) in })
    }
    
    func saveContext() {
        try? viewContext.save()
    }
}

extension CoreDataManager {
    @discardableResult func createSavedImage(with data: Data) -> SavedImage {
        let savedImage = SavedImage(context: viewContext)
        savedImage.data = data
        savedImage.creationDate = Date()
        saveContext()
        return savedImage
    }
}

Um den Managed-Object-Context des CoreDataManager im Zusammenspiel mit dem FetchRequest-Property Wrapper in SwiftUI nutzen zu können, setze ich jenen Context im Environment der ContentView.

@main
struct TS_ImagePickerView_ExampleApp: App {
    private let coreDataManager = CoreDataManager.shared
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, coreDataManager.viewContext)
        }
    }
}

Übernahme der ImagePickerView

Neben einer Option zum Speichern der Bilder benötigen wir noch ein Element, über das wir neue Bilder auswählen können. Zu diesem Zweck kommt die ImagePickerView aus der Artikelreihe zum Zugriff auf die Foto-Library zum Einsatz. An dieser View müssen wir keinerlei Veränderungen vornehmen, sie kann genau so bleiben wie sie ist. Sie ermöglicht es, einzelne Fotos aus der Library auszuwählen und die Daten eines ausgewählten Bildes an den Aufrufer zurückzugeben.

import PhotosUI
import SwiftUI

struct ImagePickerView: UIViewControllerRepresentable {
    @Binding var showsImagePickerView: Bool
    
    var action: (Data?) -> Void
    
    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }
    
    func makeUIViewController(context: Context) -> PHPickerViewController {
        var imagePickerConfiguration = PHPickerConfiguration()
        imagePickerConfiguration.filter = .images
        imagePickerConfiguration.preferredAssetRepresentationMode = .current
        imagePickerConfiguration.selectionLimit = 1
        let imagePickerViewController = PHPickerViewController(configuration: imagePickerConfiguration)
        imagePickerViewController.delegate = context.coordinator
        return imagePickerViewController
    }
    
    func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
    
    class Coordinator: NSObject, PHPickerViewControllerDelegate {
        var parent: ImagePickerView
        
        init(_ imagePickerView: ImagePickerView) {
            parent = imagePickerView
        }
        
        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            if let result = results.first {
                result.itemProvider.loadDataRepresentation(forTypeIdentifier: "public.image") { object, error in
                    self.parent.action(object)
                }
            }
            self.dismissImagePickerViewController()
        }
        
        private func dismissImagePickerViewController() {
            Task {
                await MainActor.run {
                    parent.showsImagePickerView = false
                }
            }
        }
    }
}

Speichern und Auslesen eines Bildes

Da uns nun ein Datenmodell sowie der Zugriff auf die Foto-Library zur Verfügung stehen, müssen wir beide Elemente abschließend noch zusammenbringen.

Zu diesem Zweck passe ich die ContentView so an, dass sie ein Grid aus SavedImage-Instanzen darstellt. Die SavedImage-Instanzen werden hierbei über eine FetchRequest-Property geladen und nach Datum sortiert.

Die anzuzeigenden Bilder lassen sich mittels Sheet auswählen. Über ein Button in der Toolbar lässt sich so die ImagePickerView einblenden. Nach der Auswahl eines Bildes erhält man eine Data-Instanz. Die wird genutzt, um daraus eine neue SavedImage-Instanz zu erzeugen und dem Manged-Object-Context hinzuzufügen.

struct ContentView: View {
    @FetchRequest(
        entity: SavedImage.entity(),
        sortDescriptors: [NSSortDescriptor(key: "creationDate", ascending: true)]
    )
    private var savedImages: FetchedResults<SavedImage>
    
    @State private var showImagePickerView = false
    
    var body: some View {
        NavigationView {
            ScrollView {
                LazyVGrid(columns: [GridItem(.adaptive(minimum: 120, maximum: 200))]) {
                    ForEach(savedImages) { savedImage in
                        if let savedImageData = savedImage.data, let uiImage = UIImage(data: savedImageData) {
                            Image(uiImage: uiImage)
                                .resizable()
                                .scaledToFit()
                        }
                    }
                }
                .padding()
            }
            .navigationTitle("Images")
            .toolbar {
                ToolbarItem(placement: .bottomBar) {
                    Button("Select photo") {
                        showImagePickerView = true
                    }
                }
            }
        }
        .sheet(isPresented: $showImagePickerView) {
            ImagePickerView(showsImagePickerView: $showImagePickerView) { imageData in
                if let data = imageData {
                    CoreDataManager.shared.createSavedImage(with: data)
                }
            }
        }
    }
}

Durch das direkte Zusammenspiel von Core Data-Stack, Bildauswahl und Grid werden neu ausgewählte Bilder automatisch dem Grid hinzugefügt und persistent in der App gespeichert. Nach einem Neustart der App sind demnach alle zuvor gewählten Bilder noch vorhanden und werden umgehend wieder im Grid angezeigt.

Fazit

Dieser Artikel hat beispielhaft gezeigt, wie sich Bilder auf Basis einer Data-Instanz persistent über Core Data speichern und auslesen lassen. In einem der folgenden Artikel betrachten wir ein alternatives Vorgehen, bei dem das Bild als Datei im File-System hinterlegt und über Core Data auf das Bild referenziert wird.

Den kompletten Code dieses Projekt findet ihr auf GitHub unter https://github.com/Sillivan88/TS-ImagePickerView-Example/tree/coreDataImageStorage. Beachtet hierbei, den Branch coreDataImageStorage auszuchecken.

Euer Thomas

Weiterführende Links zum Artikel


Kommentare

Schreibe einen Kommentar

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