I am having an issue with saving images to SwiftData and I'm not entirely sure of the best way to do so.
I have an EntryModel data model for an individual entry as such:
@Model
class EntryModel {
var type: EntryType
var who: UserModel
var what: String
var value: Double? = nil
var note: String
@Attribute(.externalStorage) var images: [Data]?
var when: Date = Date.now
init(type: EntryType, who: UserModel, what: String, note: String, images: [Data] = [], when: Date) {
self.type = type
self.who = who
self.what = what
self.note = note
self.images = images
self.when = when
}
Then I have a NewEntryView that allows users to create a new entry and attach some optional images for extra information. The code looks as such:
struct New EntryView {
@State private var entryItems = [PhotosPickerItem]()
@State private var entryImages = [Image]()
// Other code pertaining to different parts of my form
HStack {
PhotosPicker(selection: $entryItems, maxSelectionCount: 5, matching: .images) {
Image(systemName: "plus")
.font(.title)
.foregroundStyle(.adaptiveBlack)
.frame(width: 80, height: 70)
.background(
RoundedRectangle(cornerRadius: 10)
.stroke(lineWidth: 2)
.fill(selectedEntryType.color)
.frame(width: 70, height: 60)
)
}
ScrollView(.horizontal) {
HStack(spacing: 10) {
ForEach(0..<entryImages.count, id: \.self) { i in
entryImages[i]
.resizable()
.scaledToFill()
.clipShape(RoundedRectangle(cornerRadius: 10))
.frame(height: 60)
}
}
.onChange(of: entryItems) {
Task {
entryImages.removeAll()
for item in entryItems {
if let image = try? await item.loadTransferable(type: Image.self) {
entryImages.append(image)
}
}
}
}
// More code
Button {
let entry = EntryModel(type: selectedEntryType, who: selectedUser!, what: titleTextField, note: noteTextField, when: selectedDate)
modelContext.insert(entry)
} label: {
// Custom label
}
Now with this current implementation, I can indeed select images from my photo album and have them populate in the HStack.
When the button is clicked at the bottom the other information that the user fills in also gets saved to SwiftData and can be used throughout the app.
The issue I have is saving these images to SwiftData to be used throughout the app.
Do I insert them into the model context like I do the other values? If so how?
If not, then how would I change my current code?
Any help would be appreciated.
As an aside and not sure of it's relevancy but ever since I implemented the PhotosPicker I get the following warning in Xcode:
PHPickerViewControllerDelegate_Private doesn't respond to _pickerDidPerformConfirmationAction:
As I already showed you in my previous answer to your question, use another model for the images Data, with a relationship to the EntryModel.
Here is a fully working example code:
@main
struct TestApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: EntryModel.self)
}
}
@Model class UserModel {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
@Model class ImageData {
@Attribute(.externalStorage)
var data: Data
var entry: EntryModel?
init(data: Data) {
self.data = data
}
}
@Model class EntryModel {
var type: String
var who: UserModel
var what: String
var value: Double? = nil
var note: String
var when: Date = Date.now
@Relationship(deleteRule: .cascade, inverse: \ImageData.entry) var images: [ImageData]?
init(type: String, who: UserModel, what: String, value: Double? = nil, note: String, images: [ImageData]? = nil, when: Date) {
self.type = type
self.who = who
self.what = what
self.value = value
self.note = note
self.images = images
self.when = when
}
}
struct ContentView: View {
@Environment(\.modelContext) private var modelContext // <--- here
@State private var entryItems = [PhotosPickerItem]()
@State private var entryImages = [Image]()
@State private var imagesData = [ImageData]() // <--- here
var body: some View {
VStack {
Text("Select images first").font(.title)
PhotosPicker(selection: $entryItems, maxSelectionCount: 5, matching: .images) {
Image(systemName: "plus")
.font(.title)
.foregroundStyle(.secondary)
.frame(width: 80, height: 70)
ScrollView(.horizontal) {
HStack(spacing: 10) {
ForEach(0..<entryImages.count, id: \.self) { i in
entryImages[i]
.resizable()
.scaledToFit()
.clipShape(RoundedRectangle(cornerRadius: 10))
.frame(height: 60)
}
}
.onChange(of: entryItems) {
// --- here
Task { @MainActor in
entryImages.removeAll()
for item in entryItems {
if let imageData = try? await item.loadTransferable(type: Data.self) {
imagesData.append(ImageData(data: imageData))
if let uiImage = UIImage(data: imageData) { entryImages.append(Image(uiImage: uiImage))
}
}
}
}
}
}
}
// example code
Button("Save Images to SwiftData") {
let entryModel = EntryModel(type: "type",
who: UserModel(name: "test-user", age: 32),
what: "what",
note: "note",
images: imagesData, // <--- here
when: Date())
modelContext.insert(entryModel) // <--- here
}.buttonStyle(.bordered)
Text("Images from SwiftData")
ImagesView()
}
}
}
// example code to display the saved images
struct ImagesView: View {
@Environment(\.modelContext) private var modelContext
@Query private var entries: [EntryModel]
var body: some View {
ForEach(entries) { entry in
ScrollView (.horizontal) {
HStack {
if let imagesData = entry.images {
ForEach(imagesData) { imgData in
if let uimg = UIImage(data: imgData.data) {
Image(uiImage: uimg).resizable()
.frame(width: 123, height: 123)
}
}
}
}
}
}.padding()
}
}