Eugene Alexeev
Eugene Alexeev

Reputation: 1320

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!

Upvotes: 0

Views: 491

Answers (2)

Eugene Alexeev
Eugene Alexeev

Reputation: 1320

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

Upvotes: 2

Here is my test code, to select a video using PhotosPicker and display it in a View.

struct Movie: Transferable {
    let url: URL
    
    static var transferRepresentation: some TransferRepresentation {
        FileRepresentation(contentType: .movie) { movie in
            SentTransferredFile(movie.url)
        } importing: { received in
            
            print("----> in Movie received: \(received.file)")
            
            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)
        }
    }
}

struct GalleryView: View {
    @State private var selectedItem: PhotosPickerItem?
    @State private var player = AVPlayer()
    
    var body: some View {
        VStack {
            VideoPlayer(player: player)
                .frame(width: 345, height: 345)
                .padding(20)
            
            PhotosPicker(selection: $selectedItem, matching: .videos) {
                Image(systemName: "video.circle").resizable()
                    .frame(width: 55, height: 55)
            }
        }
        .onChange(of: selectedItem) {
            Task {
                do {
                    if let movie = try await selectedItem?.loadTransferable(type: Movie.self) {
                        print("----> in GalleryView movie: \(movie.url)")
                        player = AVPlayer(url: movie.url)
                    } else {
                        print("----> GalleryView failed...")
                    }
                } catch {
                    print("----> GalleryView error: \(error)")
                }
            }
        }
    }
}

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

Upvotes: 1

Related Questions