swiftswiftuiimagepickerphotosui

How to carry an Image Picker selection to another view?


I have a camera view where users can either take a photo with their camera (which opens with the view) or click the photo library icon and select an image from their library. If they take the photo right then, I am able to bind capturedImage to UploadPostView, but I cannot figure out how to do the same if they choose a photo from their library. I'm creating something similar to Instagram stories or Snapchat where after you take/select the photo you are able to edit it (UploadPostView) before posting.

Binding the variable in the same way I do for capturedImage does not work. I am not super familiar but I figured it was because I was binding the actual ImagePicker class. but when I try to bind ImagePicker.image or ImagePicker.imageSelection... it also doesn't do anything. Thank you!!

CustomCameraView (where the user takes the photo or selects it from their library)

import SwiftUI
import PhotosUI

struct CustomCameraView: View {
    let cameraService = CameraService()
    @Environment(\.dismiss) private var dismiss
    @StateObject var imagePicker = ImagePicker()
    @Binding var capturedImage: UIImage?

    var body: some View {
        
        //if photo taken
        if (imagePicker.image != nil) || (capturedImage != nil) {
            UploadPostView(capturedImage: $capturedImage)
        }
        
        //if photo not taken yet
        else {
            ZStack (alignment: .topLeading) {
                
                CameraView(cameraService: cameraService) { result in
                    switch result {
                    case .success(let photo):
                        if let data = photo.fileDataRepresentation() {
                            capturedImage = UIImage(data: data)
                        } else {
                            print("Error: no image data found")
                        }
                    case .failure(let err):
                        print(err.localizedDescription)
                    }
                }
                VStack (alignment: .leading) {
                    Button {
                        dismiss()
                    } label: {
                        Image("xmark")
                            .renderingMode(.template)
                            .resizable()
                            .frame(width: 28, height: 28)
                            .foregroundColor(.white)
                    }
                    
                    .padding()
                    Spacer()
                    
                    HStack {
                        PhotosPicker(selection: $imagePicker.imageSelection) {
                            Image("image-square")
                                .renderingMode(.template)
                                .resizable()
                                .frame(width: 32, height: 28)
                                .foregroundColor(.white)
                        }
                        Spacer()
                        Button {
                            cameraService.capturePhoto()
                        } label: {
                            Image(systemName: "circle")
                                .font(.system(size: 72))
                                .foregroundColor(.white)
                        }
                        Spacer()
                        Rectangle()
                            .foregroundColor(.clear)
                            .frame(width: 32, height: 28)
                    }
                    .padding()
                }
            }
            .cornerRadius(6)
            .background(.black)
        }
    }
}

ImagePicker

import SwiftUI
import PhotosUI

@MainActor
class ImagePicker: ObservableObject {
    
    @Published var image: Image?
    @Published var uiImage: UIImage?

    @Published var imageSelection: PhotosPickerItem? {
        
        didSet {
            if let imageSelection {
                Task {
                    try await loadTransferable(from: imageSelection)
                }
            }
        }
    }
    
    
    func loadTransferable(from imageSelection: PhotosPickerItem?) async throws {
        do {
            if let data = try await imageSelection?.loadTransferable(type: Data.self) {
                if let uiImage = UIImage(data: data) {
                    self.uiImage = uiImage
                    self.image = Image(uiImage: uiImage)
                }
            }
        } catch {
            print(error.localizedDescription)
            image = nil
        }
    }
}

UploadPostView (where users can edit their photo before uploading)

import SwiftUI
import Kingfisher
import PhotosUI

struct UploadPostView: View {
    @Environment(\.dismiss) private var dismiss
    @ObservedObject var viewModel = UploadPostViewModel()
    @State var caption = ""
    @State var rating = 0
    @StateObject var imagePicker = ImagePicker()
    @Binding var capturedImage: UIImage?
    
    var body: some View {
            VStack {
                    if let image = imagePicker.image {
                        image
                            .resizable()
                            .aspectRatio(contentMode: .fill)
                            .frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity)
                            .cornerRadius(6)
                            .clipped()
                    }
                    else {
                        if let image = capturedImage {
                            Image(uiImage: image)
                                .resizable()
                                .aspectRatio(contentMode: .fill)
                                .frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity)
                                .cornerRadius(6)
                                .clipped()
                        }
                    }
                    
            
                HStack {
                    Spacer()
                    Button {
                        if let uiimage = imagePicker.uiImage {
                            viewModel.uploadPost(caption: caption, image: uiimage, rating: rating)
                            viewModel.loading = true
                        } else if let uiimage = capturedImage {
                            viewModel.uploadPost(caption: caption, image: uiimage, rating: rating)
                            viewModel.loading = true
                        }
                    } label: {
                        if viewModel.loading {
                            ProgressView()
                                .progressViewStyle(CircularProgressViewStyle(tint: .white))
                                .frame(width: 24, height: 24)
                                .padding()
                                .background(Color.accentColor)
                                .foregroundColor(.white)
                                .clipShape(Circle())
                        } else {
                            Image("send-fill")
                                .renderingMode(.template)
                                .resizable()
                                .frame(width: 24, height: 24)
                                .padding()
                                .background(Color.accentColor)
                                .foregroundColor(.white)
                                .clipShape(Circle())
                        }
                    }
                }.padding(8)
            }
        .background(.black)
        .onReceive(viewModel.$didUploadPost) { success in
            if success {
                dismiss()
            }
        }
        
    }
}


Solution

  • There are 3 issues with your code.

    1. You have multiple sources of truth. Such as multiple instances of ImagePicker and having multiple variables for an image (capturedImage, uiImage and image)

    Every time you call ImagePicker() you create a different instance and one does not know about the other.

    1. The multiple sources of truth lead to the second issue of having too many conditionals.

    There are many ways to solve this but I prefer self contained reusable modules when possible.

    PhotosPicker { result in
        switch result {
        case .success(let image):
            capturedImage = image
        case .failure(let error):
            print(error)//Provides a better description of an error.
            capturedImage = nil
        }
    }
    

    You can achieve this by putting all the code for the Picker in its own View.

    import PhotosUI
    struct PhotosPicker: View{
        @State private var imageSelection: PhotosPickerItem?
        //
        let action: (Result<UIImage, Error>) async -> Void
        var body: some View{
            PhotosUI.PhotosPicker(selection: $imageSelection, matching: .images) {
                Image(systemName: "photo")
                    .renderingMode(.template)
                    .resizable()
                    .frame(width: 32, height: 28)
                    .foregroundColor(.white)
            }.task(id: imageSelection) {//Will trigger when there is a change to imageSelection
                if let _ = imageSelection{
                    do{
                        let image = try await loadTransferable()
                        await action(.success(image))
                    }catch{
                        await action(.failure(error))
                    }
                }
            }
        }
        func loadTransferable() async throws -> UIImage {
            guard let data = try await imageSelection?.loadTransferable(type: Data.self) else{
                throw PickerError.unableGetData
            }
            //Make sure you don't overlook `else` when dealing with conditionals. The user should always be informed
            guard let uiImage = UIImage(data: data) else {
                throw PickerError.unableToCreateUIImage
            }
            return uiImage
        }
        enum PickerError: LocalizedError{
            case unableGetData
            case unableToCreateUIImage
        }
    }
    

    With the above approach you eliminate the need for ImagePicker and start working directly with capturedImage.

    Now this leads to the third issue. You code is small now and all fresh in your mind but if you have to revisit this code in in a few months you will likely encounter areas that are fragile. Such as conditionals that fall through or in other words you overlook else. The first one was in the loadTransferable function and the next is in UploadPostView.

    Assuming that your plan is to never show UploadPostView unless there is a capturedImage you can improve your nil check to eliminate the optional in that View

    struct CustomCameraView: View {
        @Environment(\.dismiss) private var dismiss
        @Binding var capturedImage: UIImage?
        
        var body: some View {
            //Unwrap Binding, to get rid of the optional below
            if let b = Binding($capturedImage) {
                UploadPostView(capturedImage: b)
            }else {
                ZStack (alignment: .topLeading) {
                    
                    VStack (alignment: .leading) {
                        Button {
                            dismiss()
                        } label: {
                            Image(systemName: "xmark")
                                .renderingMode(.template)
                                .resizable()
                                .frame(width: 28, height: 28)
                                .foregroundColor(.white)
                        }
                        
                        .padding()
                        Spacer()
                        
                        HStack {
                            PhotosPicker { result in
                                switch result {
                                case .success(let image):
                                    capturedImage = image
                                case .failure(let error):
                                    print(error)
                                    capturedImage = nil
                                }
                            }
                            Spacer()
                            Button {
                                // cameraService.capturePhoto()
                            } label: {
                                Image(systemName: "circle")
                                    .font(.system(size: 72))
                                    .foregroundColor(.white)
                            }
                            Spacer()
                            Rectangle()
                                .foregroundColor(.clear)
                                .frame(width: 32, height: 28)
                        }
                        .padding()
                    }
                }
                .cornerRadius(6)
                .background(.black)
            }
        }
    }
    

    This will clean up the View significantly.

    struct UploadPostView: View {
        @Environment(\.dismiss) private var dismiss
        
        @Binding var capturedImage: UIImage //Optional removed
        
        var body: some View {
            VStack {
                
                Image(uiImage: capturedImage)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity)
                    .cornerRadius(6)
                    .clipped()
                
            }
            .background(.black)
        }
    }