How to display PhotosPicker via swiftUI Image LongPressGesture?

213 Views Asked by At

I would like to display a PhotosUI PhotosPicker via a longPressGesture of a UIImage.

import SwiftUI
import PhotosUI

@MainActor
final class TestViewModelProfile: ObservableObject {
    
    @Published private(set) var selectedImage: UIImage? = nil
    @Published var imageSelection: PhotosPickerItem? = nil {
        didSet {
            setImage(from: imageSelection)
        }
    }
    
    private func setImage(from selection: PhotosPickerItem?) {
        guard let selection else { return }
        
        Task {
            if let data = try? await selection.loadTransferable(type: Data.self) {
                if let uiImage = UIImage(data: data) {
                    selectedImage = uiImage
                    return
                }
            }
        }
    }
    
}


struct ProfilePageViewTest: View {
    
    
    @State var profilePhoto: Image?
    
    @StateObject private var viewModel = PhotoPickerViewModel()
    

    
    var body: some View {
        VStack {
            
            if let image = viewModel.selectedImage {
                Image(uiImage: image)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: 100, height: 100)
                    .clipShape(Circle())
                    .gesture(
                        LongPressGesture(minimumDuration: 1.0)
                            .onEnded { _ in
                                // Perform image upload logic here
                                uploadProfilePhoto()
                            }
                    )
                
            } else {
                Image("No Profile Picture")
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: 100, height: 100)
                    .clipShape(Circle())
                    .gesture(
                        LongPressGesture(minimumDuration: 1.0)
                            .onEnded { _ in
                                // Perform image upload logic here
                                uploadProfilePhoto()
                            }
                    )
                
                
                //PhotosPicker can be presented like this, but just via selection of the text as its own link
                PhotosPicker(selection: $viewModel.imageSelection, matching: .images) {
                    
                    Text("Open the photoPicker")
                }
                
            }
            
        }
    
        
    }
    
    func uploadProfilePhoto() {
        //present the PhotosPicker
    }
}


    



struct ProfilePageViewTest_Previews: PreviewProvider {
    static var previews: some View {
        ProfilePageViewTest().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

In the example code I have both the unsatisfactory current approach, where the PhotosPicker can be opened via selection of the "Open the photoPicker" text below the image, and a placeholder for a function which I would like to have open the photopicker via long press.

The issue is I do not how to simply present the PhotoPicker as a result of a function, rather than manually creating, effectively, a button via the body of PhotosPicker(...){ <body> }.

The end goal is actually to have uploadProfilePhoto first display a VStack of a few buttons: "Change profile photo", "View", "Cancel" and have the first one actually display the PhotosPicker, but if I can understand how to simply display the PhotosPicker as result of function, I can incorporate it into this overlayed button view.

2

There are 2 best solutions below

7
On BEST ANSWER

You could try a different approach to achieve your end goal, of ...first display a VStack of a few buttons: "Change profile photo", "View", "Cancel" and have the first one actually display the PhotosPicker

Using a .sheet and a PhotosPicker as a Button, as shown in the example code

import Foundation
import SwiftUI
import PhotosUI


struct ContentView: View {
    var body: some View {
        ProfilePageViewTest()
    }
}

@MainActor
final class PhotoPickerViewModel: ObservableObject {
    
    @Published private(set) var selectedImage: UIImage? = nil
    @Published var imageSelection: PhotosPickerItem? = nil {
        didSet {
            setImage(from: imageSelection)
        }
    }
    
    private func setImage(from selection: PhotosPickerItem?) {
        guard let selection else { return }
        
        Task {
            if let data = try? await selection.loadTransferable(type: Data.self) {
                if let uiImage = UIImage(data: data) {
                    selectedImage = uiImage
                }
            }
        }
    }
    
}

struct ProfilePageViewTest: View {
    @State var profilePhoto: Image?
    @StateObject private var viewModel = PhotoPickerViewModel()
    @State var showSelector = false
    
    var body: some View {
        VStack {
            if let image = viewModel.selectedImage {
                Image(uiImage: image)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: 100, height: 100)
                    .clipShape(Circle())
            } else {
                Image(systemName: "photo") // for testing
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: 100, height: 100)
                    .clipShape(Circle())
            }
        }
        .onLongPressGesture {
            showSelector = true
        }
        .sheet(isPresented: $showSelector) {
            VStack (spacing: 55){
                PhotosPicker(selection: $viewModel.imageSelection, matching: .images) {
                    Text("Change profile photo")
                }
                Button("View") {
                    // ...
                }
                Button("Cancel") {
                    // ...
                    showSelector = false
                }
            }.buttonStyle(.bordered)
            .onChange(of: viewModel.imageSelection) { //<-- here
                showSelector = false
            }
            // .onChange(of: viewModel.imageSelection) { _ in  // <-- here
            //    showSelector = false
            //  }
        }
    }
    
}

EDIT-1

You can of course use a simple if, such as:

struct ProfilePageViewTest: View {
    @State var profilePhoto: Image?
    @StateObject private var viewModel = PhotoPickerViewModel()
    @State var showSelector = false 
    
    var body: some View {
        VStack {
            if let image = viewModel.selectedImage {
                Image(uiImage: image)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: 100, height: 100)
                    .clipShape(Circle())
            } else {
                Image(systemName: "photo") // for testing
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: 100, height: 100)
                    .clipShape(Circle())
            }
            Spacer()
            if showSelector {  // <--- here
                VStack (spacing: 55){
                    PhotosPicker(selection: $viewModel.imageSelection, matching: .images) {
                        Text("Change profile photo")
                    }
                    Button("View") {
                        // ...
                        showSelector = false
                    }
                    Button("Cancel") {
                        // ...
                        showSelector = false
                    }
                }
                .buttonStyle(.bordered)
            }
        }
        .padding(.top, 30)
        .onLongPressGesture{
            showSelector = true
        }
    }
}

EDIT-2:

using a simple tap on the picture, to choose/change the profile photo.

struct ProfilePageViewTest: View {
    @State var profilePhoto: Image?
    @StateObject private var viewModel = PhotoPickerViewModel()
    
    var body: some View {
        VStack {
            PhotosPicker(selection: $viewModel.imageSelection, matching: .images) {
                if let image = viewModel.selectedImage {
                    Image(uiImage: image)
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .frame(width: 100, height: 100)
                        .clipShape(Circle())
                } else {
                    Image(systemName: "photo")
                        .resizable()
                        .aspectRatio(contentMode: .fill)
                        .frame(width: 100, height: 100)
                        .clipShape(Circle())
                }
            }
        }
    }
}
0
On

You can use

.photosPicker(isPresented: $isShowingPicker, selection: $viewModel.imageSelection)

Instead of

 PhotosPicker(selection: $viewModel.imageSelection, matching: .images)

Here is a full set of code.

import SwiftUI
import PhotosUI

@MainActor
final class TestViewModelProfile: ObservableObject {
    @Published private(set) var selectedImage: UIImage? = nil
    @Published var imageSelection: PhotosPickerItem? = nil
    
    func setImage() async throws {
        guard let imageSelection else { return }
        
        guard let data = try await imageSelection.loadTransferable(type: Data.self) 
        else {throw LocalError.missingImageData}
        
        guard let uiImage = UIImage(data: data) 
        else { throw LocalError.unableToCreateImage }
        
        selectedImage = uiImage
    }
    private enum LocalError: LocalizedError {
        case missingImageData
        case unableToCreateImage
    }
}


struct ProfilePageViewTest: View {
    @State private var profilePhoto: Image?
    @StateObject private var viewModel = TestViewModelProfile()
    @State private var isShowingPicker: Bool = false
    
    var body: some View {
        VStack {
            if let image = viewModel.selectedImage {
                Image(uiImage: image)
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: 100, height: 100)
                    .clipShape(Circle())
                    .gesture(
                        LongPressGesture(minimumDuration: 1.0)
                            .onEnded { _ in
                                // Perform image upload logic here
                                uploadProfilePhoto()
                            }
                    )
                
            } else {
                Image(systemName: "person")
                    .resizable()
                    .aspectRatio(contentMode: .fill)
                    .frame(width: 100, height: 100)
                    .clipShape(Circle())
                    .gesture(
                        LongPressGesture(minimumDuration: 1.0)
                            .onEnded { _ in
                                // Perform image upload logic here
                                uploadProfilePhoto()
                            }
                    )
            }
        }.photosPicker(isPresented: $isShowingPicker, selection: $viewModel.imageSelection)
            .task(id: viewModel.imageSelection) {
                do {
                    try await viewModel.setImage()
                } catch {
                    print (error) //Show error to user.
                }
            }
    }
    
    func uploadProfilePhoto() {
        isShowingPicker.toggle()
    }
}


#Preview {
    ProfilePageViewTest()
}