iosswiftswiftuiphotosui

Adding multiple images into a view from photo library - SwiftUI


I want to add images from phone's photo library into a collage layout that I made. First I made the collage layout as a separate view in SwiftUI called CollageLayoutOne.

import SwiftUI

struct CollageLayoutOne: View {
    
    var uiImageOne: UIImage
    var uiImageTwo: UIImage
    var uiImageThree: UIImage
    
    var body: some View {
        
        Rectangle()
            .fill(Color.gray)
            .aspectRatio(1.0, contentMode: .fit)
            .overlay {
                HStack {
                    Rectangle()
                        .fill(Color.gray)
                        .overlay {
                            Image(uiImage: uiImageOne)
                                .resizable()
                                .aspectRatio(contentMode: .fill)
                        }
                        .clipped()
                    VStack {
                        Rectangle()
                            .fill(Color.gray)
                            .overlay {
                                Image(uiImage: uiImageTwo)
                                    .resizable()
                                    .aspectRatio(contentMode: .fill)
                            }
                            .clipped()
                        Rectangle()
                            .fill(Color.gray)
                            .overlay {
                                Image(uiImage: uiImageThree)
                                    .resizable()
                                    .aspectRatio(contentMode: .fill)
                            }
                            .clipped()
                    }
                }
                .padding()
            }
    }
}

Then I have a separate view (PageView) where I want to show the CollageLayoutOne view and it also hosts the button to get to the image library.

struct PageView: View {
    
    @State private var photoPickerIsPresented = false
    @State var pickerResult: [UIImage] = []
    
    var body: some View {
      NavigationView {
        ScrollView {
            if pickerResult.isEmpty {
                
            } else {
                CollageLayoutOne(uiImageOne: pickerResult[0], uiImageTwo: pickerResult[1], uiImageThree: pickerResult[2])
            }
            
            
        }
        .edgesIgnoringSafeArea(.bottom)
        .navigationBarTitle("Select Photo", displayMode: .inline)
        .navigationBarItems(trailing: selectPhotoButton)
        .sheet(isPresented: $photoPickerIsPresented) {
          PhotoPicker(pickerResult: $pickerResult,
                      isPresented: $photoPickerIsPresented)
        }
      }
    }
    
    @ViewBuilder
    private var selectPhotoButton: some View {
      Button(action: {
        photoPickerIsPresented = true
      }, label: {
        Label("Select", systemImage: "photo")
      })
    }
    
}

My problem is that for some unknown reason the app crashes every time I select the photos and try to add them. If I do pickerResult[0] for all three it works just fine, but displays only the first selected photo on all 3 spots. Also if I start with all 3 as pickerResult[0] and then change them to [0], [1], [2] while the preview is running it doesn't crash and displays correctly.

I'm just starting with Swift and SwiftUI, so excuse me if it's some elementary mistake. Below I am also adding my code for PhotoPicker that I got from an article I found.

enter image description here

PhotoPicker.swift:

import SwiftUI
import PhotosUI

struct PhotoPicker: UIViewControllerRepresentable {
  @Binding var pickerResult: [UIImage]
  @Binding var isPresented: Bool
  
  func makeUIViewController(context: Context) -> some UIViewController {
    var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
    configuration.filter = .images // filter only to images
    if #available(iOS 15, *) {
        configuration.selection = .ordered //number selection
    }
    configuration.selectionLimit = 3 // ignore limit
    
    let photoPickerViewController = PHPickerViewController(configuration: configuration)
    photoPickerViewController.delegate = context.coordinator
    return photoPickerViewController
  }
  
  func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { }
  
  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }
  
  class Coordinator: PHPickerViewControllerDelegate {
    private let parent: PhotoPicker
    
    init(_ parent: PhotoPicker) {
      self.parent = parent
    }
    
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
      parent.pickerResult.removeAll()
      
      for image in results {
        if image.itemProvider.canLoadObject(ofClass: UIImage.self) {
          image.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] newImage, error in
            if let error = error {
              print("Can't load image \(error.localizedDescription)")
            } else if let image = newImage as? UIImage {
              self?.parent.pickerResult.append(image)
            }
          }
        } else {
          print("Can't load asset")
        }
      }
      
      parent.isPresented = false
    }
  }
}

Solution

  • image.itemProvider.loadObject is an asynchronous function, and it loads images one by one.

    When the first image is processed, you add it to pickerResult and your pickerResult.isEmpty check becomes false, but your array contains only one item so far.

    The safe thing to do here is to check the count:

    if pickerResult.count == 3 {
        CollageLayoutOne(uiImageOne: pickerResult[0], uiImageTwo: pickerResult[1], uiImageThree: pickerResult[2])
    }
    

    Also, in such cases, it's a good idea to wait until all asynchronous requests are complete before updating the UI, for example, like this:

    var processedResults = [UIImage]()
    var leftToLoad = results.count
    let checkFinished = { [weak self] in
        leftToLoad -= 1
        if leftToLoad == 0 {
            self?.parent.pickerResult = processedResults
            self?.parent.isPresented = false
        }
    }
    for image in results {
        if image.itemProvider.canLoadObject(ofClass: UIImage.self) {
            image.itemProvider.loadObject(ofClass: UIImage.self) { newImage, error in
                if let error = error {
                    print("Can't load image \(error.localizedDescription)")
                } else if let image = newImage as? UIImage {
                    processedResults.append(image)
                }
                checkFinished()
            }
        } else {
            print("Can't load asset")
            checkFinished()
        }
    }