swiftuiswiftdataphoto-pickerphotosui

How to save images from a Photo Picker to SwiftData


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:


Solution

  • 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()
        }
    }