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