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.
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
- Beispielprojekt auf GitHub: https://github.com/Sillivan88/TS-ImagePickerView-Example/tree/coreDataImageStorage
- Ergänzendes YouTube-Video: https://www.youtube.com/watch?v=qR1GWs5ve3M
Schreibe einen Kommentar