iosswiftuivideogalleryphotospicker

PhotosPicker freezes at loading stage (SwiftUI)


The issue is - I am trying to load video from the gallery to my app but I can not succeed. Here are relevant pieces of code

This is my Transferable struct

struct Movie: Transferable {
    let url: URL

    static var transferRepresentation: some TransferRepresentation {
        FileRepresentation(contentType: .movie) { movie in
            SentTransferredFile(movie.url)
        } importing: { received in
            let copy = URL.documentsDirectory.appending(path: "movie.mp4")

            if FileManager.default.fileExists(atPath: copy.path()) {
                try FileManager.default.removeItem(at: copy)
            }

            try FileManager.default.copyItem(at: received.file, to: copy)
            return Self.init(url: copy)
        }
    }
}

This is my SwiftUI code

struct GalleryView: View {
    
    
    @State private var selectedItem: PhotosPickerItem?

    ....

    // Somewhere in ZStack of GalleryView
    HStack {
                
        Spacer()
                 
        VStack {
                    
            Spacer()
                    
            PhotosPicker(
               selection: $selectedItem,
               matching: .videos)
            {
                Image("btn_plus")
            }
            .padding(.bottom, 16)
                    
            }
            .padding(.trailing, 16)
      }

      ..........

      .onChange(of: selectedItem) { newValue in
            Task {
                do {
                    print("loading!!!...")
                    
                    if let movie = try await selectedItem?.loadTransferable(type: Movie.self) {
                        print("loaded!!!...")
                    } else {
                        print("failed!!!...")
                    }
                } catch {
                    print("failed with ex!!!...")
                }
            }
        }

So when I run the code, I see my button, I press it, Gallery opens up with videos only (as intented). Then I pick some video and I see this log

loading!!!...

And execution goes into loadTransferable and never comes back. I am pretty sure I made some newbie mistake because this is the first time I am attaching this functionality in SwiftUI. Could you please point me out what did I do wrong? Thank you!


Solution

  • Well, now it works. Why is that? No idea. The only assumption I have is that I added photoLibrary: .shared()), so PhotosPicker code looks like this:

    PhotosPicker(
        selection: $viewModel.videoPicker.videoSelection,
        matching: .videos,
        photoLibrary: .shared())
        {
            Image("btn_plus")
        }
            .padding(.bottom, 16)
    

    But when I removed photoLibrary: .shared() just to make sure if this is the reason, code also works... Maybe the first call with photoLibrary: .shared() engaged granting permissions to the app to view files in Gallery? I don't know... but there you go. I am not marking this post as an answer to my question, because it is clearly not.

    UPDATE:

    OMG, how inconsistent is PhotosPicker in SwiftUI, you guys have no idea. Code above no longer works - again. Sometimes it loads the video, sometimes it doesn't. Sometimes it looks like it depends on longevity or size of the video, but sometimes it can not load 5 second video, but can load 10 minutes video.

    Finally I fixed it by implementing UIKit solution with PHPickerViewController. It is very stable, and uploads absolutely any video I pick from the Gallery and does it consistently. Feel free to use it:

    //
    //  PickVideoFromGalleryView.swift
    //  spintip-iOS-app
    //
    //  Created by Yevhen Alieksieiev on 14.05.2024.
    //
    
    import SwiftUI
    import PhotosUI
    import AVKit
    import Combine
    
    final class PickVideoFromGalleryCoordinator: NSObject {
        var parent: PickVideoFromGalleryView
     
        init(_ parent: PickVideoFromGalleryView) {
            self.parent = parent
        }
    }
    
    extension PickVideoFromGalleryCoordinator: PHPickerViewControllerDelegate {
        func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
            picker.dismiss(animated: true)
            
            guard let pickedSelection = results.first else {
                self.parent.errorMsg = "No video was selected"
                return
            }
            
            let itemProvider = pickedSelection.itemProvider
            
            if itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
                let progress = itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { [weak self] url, error in
                    do {
                        guard let url = url, error == nil else {
                            throw error ?? NSError(domain: NSFileProviderErrorDomain, code: -1, userInfo: nil)
                        }
                        let localURL = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
                        try? FileManager.default.removeItem(at: localURL)
                        try FileManager.default.copyItem(at: url, to: localURL)
                        
                        self?.parent.videoUrl = IdentifiableURL(url: localURL)
                        
    
                    } catch let catchedError {
                        
                        self?.parent.errorMsg = catchedError.localizedDescription
                    }
                }
            } else {
                self.parent.errorMsg = "You can process videos only"
            }
        }
    }
    
    struct PickVideoFromGalleryView: UIViewControllerRepresentable {
        
        typealias UIViewControllerType = PHPickerViewController
        
        @Binding var videoUrl: IdentifiableURL?
        @Binding var errorMsg: String?
        
        public init(videoUrl: Binding<IdentifiableURL?>, errorMsg: Binding<String?>) {
            _videoUrl = videoUrl
            _errorMsg = errorMsg
        }
        
        func makeUIViewController(context: Context) -> UIViewControllerType {
            
            var configuration = PHPickerConfiguration(photoLibrary: .shared())
            
            // Set the filter type according to the user’s selection.
            configuration.filter = PHPickerFilter.videos
            // Set the mode to avoid transcoding, if possible, if your app supports arbitrary image/video encodings.
            configuration.preferredAssetRepresentationMode = .current
            // Set the selection behavior to respect the user’s selection order.
            configuration.selection = .ordered
            // Set the selection limit to enable multiselection.
            configuration.selectionLimit = 1
            
            let picker = PHPickerViewController(configuration: configuration)
            picker.delegate = context.coordinator
            
            return picker
        }
        
        // In our case we do not need to update our `AVPlayerViewController` when AVPlayer changes
        func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {}
        
        // Creates the coordinator that is used to handle and communicate changes in `AVPlayerViewController`
        func makeCoordinator() -> PickVideoFromGalleryCoordinator {
            PickVideoFromGalleryCoordinator(self)
        }
    }