snapshot of https://github.com/tauri-apps/plugins-workspace.git (v2, 2026-09-23; blobs > 131072B omitted)
Some checks failed
integration tests / run-integration-tests (windows-latest) (push) Has been cancelled
Test Rust / test (push) Has been cancelled
test plugins e2e (mobile) / ios (push) Has been cancelled
test plugins e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test plugins e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
integration tests / run-integration-tests (macos-latest) (push) Has been cancelled
version or publish / version-or-publish (push) Failing after 0s
integration tests / run-integration-tests (ubuntu-22.04) (push) Failing after 0s
Lint JavaScript / eslint (push) Failing after 0s
Sync / sync-to-mirrors (push) Failing after 0s
test plugins e2e (mobile) / android (push) Failing after 0s
test plugins e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
Test Rust / changes (push) Failing after 0s
Lint Rust / changes (push) Failing after 0s
Lint Rust / clippy (push) Has been skipped
Audit Rust / audit-rust (push) Failing after 1s
Audit JavaScript / audit-js (push) Failing after 1s
Some checks failed
integration tests / run-integration-tests (windows-latest) (push) Has been cancelled
Test Rust / test (push) Has been cancelled
test plugins e2e (mobile) / ios (push) Has been cancelled
test plugins e2e / test (map[name:Windows os:windows-latest]) (push) Has been cancelled
test plugins e2e / test (map[name:macOS os:macos-latest]) (push) Has been cancelled
integration tests / run-integration-tests (macos-latest) (push) Has been cancelled
version or publish / version-or-publish (push) Failing after 0s
integration tests / run-integration-tests (ubuntu-22.04) (push) Failing after 0s
Lint JavaScript / eslint (push) Failing after 0s
Sync / sync-to-mirrors (push) Failing after 0s
test plugins e2e (mobile) / android (push) Failing after 0s
test plugins e2e / test (map[name:Linux os:ubuntu-latest]) (push) Failing after 0s
Test Rust / changes (push) Failing after 0s
Lint Rust / changes (push) Failing after 0s
Lint Rust / clippy (push) Has been skipped
Audit Rust / audit-rust (push) Failing after 1s
Audit JavaScript / audit-js (push) Failing after 1s
This commit is contained in:
313
plugins/dialog/ios/Sources/DialogPlugin.swift
Normal file
313
plugins/dialog/ios/Sources/DialogPlugin.swift
Normal file
@@ -0,0 +1,313 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import MobileCoreServices
|
||||
import Photos
|
||||
import PhotosUI
|
||||
import SwiftRs
|
||||
import Tauri
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
import WebKit
|
||||
|
||||
enum FilePickerEvent {
|
||||
case selected([URL])
|
||||
case cancelled
|
||||
case error(String)
|
||||
}
|
||||
|
||||
struct MessageDialogOptions: Decodable {
|
||||
var title: String?
|
||||
let message: String
|
||||
var okButtonLabel: String?
|
||||
var noButtonLabel: String?
|
||||
var cancelButtonLabel: String?
|
||||
}
|
||||
|
||||
struct Filter: Decodable {
|
||||
var extensions: [String]?
|
||||
}
|
||||
|
||||
struct FilePickerOptions: Decodable {
|
||||
var multiple: Bool?
|
||||
var filters: [Filter]?
|
||||
var defaultPath: String?
|
||||
var pickerMode: PickerMode?
|
||||
var fileAccessMode: FileAccessMode?
|
||||
}
|
||||
|
||||
struct SaveFileDialogOptions: Decodable {
|
||||
var fileName: String?
|
||||
var defaultPath: String?
|
||||
}
|
||||
|
||||
enum FileAccessMode: String, Decodable {
|
||||
case copy
|
||||
case scoped
|
||||
}
|
||||
|
||||
enum PickerMode: String, Decodable {
|
||||
case document
|
||||
case media
|
||||
case image
|
||||
case video
|
||||
}
|
||||
|
||||
class DialogPlugin: Plugin {
|
||||
|
||||
var filePickerController: FilePickerController!
|
||||
var onFilePickerResult: ((FilePickerEvent) -> Void)? = nil
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
filePickerController = FilePickerController(self)
|
||||
|
||||
}
|
||||
|
||||
@objc public func showFilePicker(_ invoke: Invoke) throws {
|
||||
let args = try invoke.parseArgs(FilePickerOptions.self)
|
||||
|
||||
onFilePickerResult = { (event: FilePickerEvent) -> Void in
|
||||
switch event {
|
||||
case .selected(let urls):
|
||||
invoke.resolve(["files": urls])
|
||||
case .cancelled:
|
||||
invoke.resolve(["files": nil])
|
||||
case .error(let error):
|
||||
invoke.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 14, *) {
|
||||
let parsedTypes = parseFiltersOption(args.filters ?? [])
|
||||
|
||||
let mimeKinds = Set(
|
||||
parsedTypes.compactMap { $0.preferredMIMEType?.components(separatedBy: "/")[0] })
|
||||
let filtersIncludeImage = mimeKinds.contains("image")
|
||||
let filtersIncludeVideo = mimeKinds.contains("video")
|
||||
let filtersIncludeNonMedia = mimeKinds.contains(where: { $0 != "image" && $0 != "video" })
|
||||
|
||||
// If the picker mode is media, images, or videos, we always want to show the media picker regardless of what's in the filters.
|
||||
// Otherwise, if the filters A) do not include non-media types and B) include either image or video, we want to show the media picker.
|
||||
if args.pickerMode == .media
|
||||
|| args.pickerMode == .image
|
||||
|| args.pickerMode == .video
|
||||
|| (!filtersIncludeNonMedia && (filtersIncludeImage || filtersIncludeVideo))
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
var configuration = PHPickerConfiguration(photoLibrary: PHPhotoLibrary.shared())
|
||||
configuration.selectionLimit = (args.multiple ?? false) ? 0 : 1
|
||||
|
||||
// If the filters include image or video, use the appropriate filter.
|
||||
// If both are true, don't define a filter, which means we will display all media.
|
||||
if args.pickerMode == .image || (filtersIncludeImage && !filtersIncludeVideo) {
|
||||
configuration.filter = .images
|
||||
} else if args.pickerMode == .video || (filtersIncludeVideo && !filtersIncludeImage) {
|
||||
configuration.filter = .videos
|
||||
}
|
||||
|
||||
let picker = PHPickerViewController(configuration: configuration)
|
||||
picker.delegate = self.filePickerController
|
||||
picker.modalPresentationStyle = .fullScreen
|
||||
self.presentViewController(picker)
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
// The UTType.item is the catch-all, allowing for any file type to be selected.
|
||||
let contentTypes = parsedTypes.isEmpty ? [UTType.item] : parsedTypes
|
||||
let picker: UIDocumentPickerViewController = UIDocumentPickerViewController(
|
||||
forOpeningContentTypes: contentTypes,
|
||||
asCopy: args.fileAccessMode == .scoped ? false : true)
|
||||
|
||||
if let defaultPath = args.defaultPath {
|
||||
picker.directoryURL = URL(string: defaultPath)
|
||||
}
|
||||
|
||||
picker.delegate = self.filePickerController
|
||||
picker.allowsMultipleSelection = args.multiple ?? false
|
||||
picker.modalPresentationStyle = .fullScreen
|
||||
self.presentViewController(picker)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showFilePickerLegacy(args: args)
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func saveFileDialog(_ invoke: Invoke) throws {
|
||||
let args = try invoke.parseArgs(SaveFileDialogOptions.self)
|
||||
|
||||
// The Tauri save dialog API prompts the user to select a path where a file must be saved
|
||||
// This behavior maps to the operating system interfaces on all platforms except iOS,
|
||||
// which only exposes a mechanism to "move file `srcPath` to a location defined by the user"
|
||||
//
|
||||
// so we have to work around it by creating an empty file matching the requested `args.fileName`,
|
||||
// and using it as `srcPath` for the operation - returning the path the user selected
|
||||
// so the app dev can write to it later - matching cross platform behavior as mentioned above
|
||||
let fileManager = FileManager.default
|
||||
let srcFolder = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||
let srcPath = srcFolder.appendingPathComponent(args.fileName ?? "file")
|
||||
if !fileManager.fileExists(atPath: srcPath.path) {
|
||||
// the file contents must be actually provided by the tauri dev after the path is resolved by the save API
|
||||
try "".write(to: srcPath, atomically: true, encoding: .utf8)
|
||||
}
|
||||
|
||||
onFilePickerResult = { (event: FilePickerEvent) -> Void in
|
||||
switch event {
|
||||
case .selected(let urls):
|
||||
invoke.resolve(["file": urls.first!])
|
||||
case .cancelled:
|
||||
invoke.resolve(["file": nil])
|
||||
case .error(let error):
|
||||
invoke.reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let picker = UIDocumentPickerViewController(url: srcPath, in: .exportToService)
|
||||
if let defaultPath = args.defaultPath {
|
||||
picker.directoryURL = URL(string: defaultPath)
|
||||
}
|
||||
picker.delegate = self.filePickerController
|
||||
picker.modalPresentationStyle = .fullScreen
|
||||
self.presentViewController(picker)
|
||||
}
|
||||
}
|
||||
|
||||
private func presentViewController(_ viewControllerToPresent: UIViewController) {
|
||||
self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
private func parseFiltersOption(_ filters: [Filter]) -> [UTType] {
|
||||
var parsedTypes: [UTType] = []
|
||||
for filter in filters {
|
||||
for ext in filter.extensions ?? [] {
|
||||
// We need to support extensions as well as MIME types.
|
||||
if let utType = UTType(mimeType: ext) {
|
||||
parsedTypes.append(utType)
|
||||
} else if let utType = UTType(filenameExtension: ext) {
|
||||
parsedTypes.append(utType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parsedTypes
|
||||
}
|
||||
|
||||
/// This function is only used for iOS < 14, and should be removed if/when the deployment target is raised to 14.
|
||||
private func showFilePickerLegacy(args: FilePickerOptions) {
|
||||
let parsedTypes = parseFiltersOptionLegacy(args.filters ?? [])
|
||||
|
||||
var filtersIncludeImage: Bool = false
|
||||
var filtersIncludeVideo: Bool = false
|
||||
var filtersIncludeNonMedia: Bool = false
|
||||
|
||||
if !parsedTypes.isEmpty {
|
||||
let mimeKinds = Set(parsedTypes.map { $0.components(separatedBy: "/")[0] })
|
||||
filtersIncludeImage = mimeKinds.contains("image")
|
||||
filtersIncludeVideo = mimeKinds.contains("video")
|
||||
filtersIncludeNonMedia = mimeKinds.contains(where: { $0 != "image" && $0 != "video" })
|
||||
}
|
||||
|
||||
if !filtersIncludeNonMedia && (filtersIncludeImage || filtersIncludeVideo) {
|
||||
DispatchQueue.main.async {
|
||||
let picker = UIImagePickerController()
|
||||
picker.delegate = self.filePickerController
|
||||
|
||||
if filtersIncludeImage && !filtersIncludeVideo {
|
||||
picker.sourceType = .photoLibrary
|
||||
}
|
||||
|
||||
picker.modalPresentationStyle = .fullScreen
|
||||
self.presentViewController(picker)
|
||||
}
|
||||
} else {
|
||||
let documentTypes = parsedTypes.isEmpty ? ["public.data"] : parsedTypes
|
||||
DispatchQueue.main.async {
|
||||
let picker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
|
||||
if let defaultPath = args.defaultPath {
|
||||
picker.directoryURL = URL(string: defaultPath)
|
||||
}
|
||||
|
||||
picker.delegate = self.filePickerController
|
||||
picker.allowsMultipleSelection = args.multiple ?? false
|
||||
picker.modalPresentationStyle = .fullScreen
|
||||
self.presentViewController(picker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This function is only used for iOS < 14, and should be removed if/when the deployment target is raised to 14.
|
||||
private func parseFiltersOptionLegacy(_ filters: [Filter]) -> [String] {
|
||||
var parsedTypes: [String] = []
|
||||
for filter in filters {
|
||||
for ext in filter.extensions ?? [] {
|
||||
guard
|
||||
let utType: String = UTTypeCreatePreferredIdentifierForTag(
|
||||
kUTTagClassMIMEType, ext as CFString, nil)?.takeRetainedValue() as String?
|
||||
else {
|
||||
continue
|
||||
}
|
||||
parsedTypes.append(utType)
|
||||
}
|
||||
}
|
||||
|
||||
return parsedTypes
|
||||
}
|
||||
|
||||
public func onFilePickerEvent(_ event: FilePickerEvent) {
|
||||
self.onFilePickerResult?(event)
|
||||
}
|
||||
|
||||
@objc public func showMessageDialog(_ invoke: Invoke) throws {
|
||||
let manager = self.manager
|
||||
let args = try invoke.parseArgs(MessageDialogOptions.self)
|
||||
|
||||
DispatchQueue.main.async { [] in
|
||||
let alert = UIAlertController(
|
||||
title: args.title, message: args.message, preferredStyle: UIAlertController.Style.alert)
|
||||
|
||||
if let cancelButtonLabel = args.cancelButtonLabel {
|
||||
alert.addAction(
|
||||
UIAlertAction(
|
||||
title: cancelButtonLabel, style: UIAlertAction.Style.default,
|
||||
handler: { (_) -> Void in
|
||||
invoke.resolve(["value": cancelButtonLabel])
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if let noButtonLabel = args.noButtonLabel {
|
||||
alert.addAction(
|
||||
UIAlertAction(
|
||||
title: noButtonLabel, style: UIAlertAction.Style.default,
|
||||
handler: { (_) -> Void in
|
||||
invoke.resolve(["value": noButtonLabel])
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let okButtonLabel = args.okButtonLabel ?? "Ok"
|
||||
alert.addAction(
|
||||
UIAlertAction(
|
||||
title: okButtonLabel, style: UIAlertAction.Style.default,
|
||||
handler: { (_) -> Void in
|
||||
invoke.resolve(["value": okButtonLabel])
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
manager.viewController?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@_cdecl("init_plugin_dialog")
|
||||
func initPlugin() -> Plugin {
|
||||
return DialogPlugin()
|
||||
}
|
||||
253
plugins/dialog/ios/Sources/FilePickerController.swift
Normal file
253
plugins/dialog/ios/Sources/FilePickerController.swift
Normal file
@@ -0,0 +1,253 @@
|
||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import UIKit
|
||||
import MobileCoreServices
|
||||
import PhotosUI
|
||||
import Photos
|
||||
import Tauri
|
||||
|
||||
public class FilePickerController: NSObject {
|
||||
var plugin: DialogPlugin
|
||||
|
||||
init(_ dialogPlugin: DialogPlugin) {
|
||||
plugin = dialogPlugin
|
||||
}
|
||||
|
||||
private func dismissViewController(_ viewControllerToPresent: UIViewController, completion: (() -> Void)? = nil) {
|
||||
viewControllerToPresent.dismiss(animated: true, completion: completion)
|
||||
}
|
||||
|
||||
public func getModifiedAtFromUrl(_ url: URL) -> Int? {
|
||||
do {
|
||||
let attributes = try FileManager.default.attributesOfItem(atPath: url.path)
|
||||
if let modifiedDateInSec = (attributes[.modificationDate] as? Date)?.timeIntervalSince1970 {
|
||||
return Int(modifiedDateInSec * 1000.0)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} catch let error as NSError {
|
||||
Logger.error("getModifiedAtFromUrl failed", error.localizedDescription)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func getMimeTypeFromUrl(_ url: URL) -> String {
|
||||
let fileExtension = url.pathExtension as CFString
|
||||
guard let extUTI = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, fileExtension, nil)?.takeUnretainedValue() else {
|
||||
return ""
|
||||
}
|
||||
guard let mimeUTI = UTTypeCopyPreferredTagWithClass(extUTI, kUTTagClassMIMEType) else {
|
||||
return ""
|
||||
}
|
||||
return mimeUTI.takeRetainedValue() as String
|
||||
}
|
||||
|
||||
public func getSizeFromUrl(_ url: URL) throws -> Int {
|
||||
let values = try url.resourceValues(forKeys: [.fileSizeKey])
|
||||
return values.fileSize ?? 0
|
||||
}
|
||||
|
||||
public func getVideoDuration(_ url: URL) -> Int {
|
||||
let asset = AVAsset(url: url)
|
||||
let duration = asset.duration
|
||||
let durationTime = CMTimeGetSeconds(duration)
|
||||
return Int(round(durationTime))
|
||||
}
|
||||
|
||||
public func getImageDimensions(_ url: URL) -> (Int?, Int?) {
|
||||
if let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) {
|
||||
if let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as Dictionary? {
|
||||
return getHeightAndWidthFromImageProperties(imageProperties)
|
||||
}
|
||||
}
|
||||
return (nil, nil)
|
||||
}
|
||||
|
||||
public func getVideoDimensions(_ url: URL) -> (Int?, Int?) {
|
||||
guard let track = AVURLAsset(url: url).tracks(withMediaType: AVMediaType.video).first else { return (nil, nil) }
|
||||
let size = track.naturalSize.applying(track.preferredTransform)
|
||||
let height = abs(Int(size.height))
|
||||
let width = abs(Int(size.width))
|
||||
return (height, width)
|
||||
}
|
||||
|
||||
private func getHeightAndWidthFromImageProperties(_ properties: [NSObject: AnyObject]) -> (Int?, Int?) {
|
||||
let width = properties[kCGImagePropertyPixelWidth] as? Int
|
||||
let height = properties[kCGImagePropertyPixelHeight] as? Int
|
||||
let orientation = properties[kCGImagePropertyOrientation] as? Int ?? UIImage.Orientation.up.rawValue
|
||||
switch orientation {
|
||||
case UIImage.Orientation.left.rawValue, UIImage.Orientation.right.rawValue, UIImage.Orientation.leftMirrored.rawValue, UIImage.Orientation.rightMirrored.rawValue:
|
||||
return (width, height)
|
||||
default:
|
||||
return (height, width)
|
||||
}
|
||||
}
|
||||
|
||||
private func getFileUrlByPath(_ path: String) -> URL? {
|
||||
guard let url = URL.init(string: path) else {
|
||||
return nil
|
||||
}
|
||||
if FileManager.default.fileExists(atPath: url.path) {
|
||||
return url
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// ## In which cases do we need to save a copy of a file selected by a user to the app sandbox?
|
||||
/// In short, only when the file is **not** selected using UIDocumentPickerDelegate.
|
||||
/// For the rest of the cases, we need to write a copy of the file to the app sandbox.
|
||||
///
|
||||
/// For PHPicker (used for photos and videos), `NSItemProvider.loadFileRepresentation` returns a temporary file URL that is deleted after the completion handler.
|
||||
/// The recommendation is to [Persist](https://developer.apple.com/documentation/foundation/nsitemprovider/2888338-loadfilerepresentation) the file by moving/copying
|
||||
/// it to your app’s directory within the completion handler.
|
||||
///
|
||||
/// If available, `loadInPlaceFileRepresentation` can open a file in place; Photos assets typically do not support true in-place access,
|
||||
/// so fall back to persisting a local file.
|
||||
/// Ref: https://developer.apple.com/documentation/foundation/nsitemprovider/2888335-loadinplacefilerepresentation
|
||||
///
|
||||
/// For UIDocumentPicker, prefer "open in place" and avoid copying when possible.
|
||||
/// Ref: https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller
|
||||
private func saveTemporaryFile(_ sourceUrl: URL) throws -> URL {
|
||||
|
||||
var directory = URL(fileURLWithPath: NSTemporaryDirectory())
|
||||
if let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
|
||||
directory = cachesDirectory
|
||||
}
|
||||
|
||||
let targetUrl = directory.appendingPathComponent(sourceUrl.lastPathComponent)
|
||||
do {
|
||||
try deleteFile(targetUrl)
|
||||
}
|
||||
|
||||
try FileManager.default.copyItem(at: sourceUrl, to: targetUrl)
|
||||
return targetUrl
|
||||
}
|
||||
|
||||
private func deleteFile(_ url: URL) throws {
|
||||
if FileManager.default.fileExists(atPath: url.path) {
|
||||
try FileManager.default.removeItem(atPath: url.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FilePickerController: UIDocumentPickerDelegate {
|
||||
public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
do {
|
||||
self.plugin.onFilePickerEvent(.selected(urls))
|
||||
} catch {
|
||||
self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file"))
|
||||
}
|
||||
}
|
||||
|
||||
public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
|
||||
self.plugin.onFilePickerEvent(.cancelled)
|
||||
}
|
||||
}
|
||||
|
||||
extension FilePickerController: UIImagePickerControllerDelegate, UINavigationControllerDelegate, UIPopoverPresentationControllerDelegate {
|
||||
public func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
dismissViewController(picker)
|
||||
self.plugin.onFilePickerEvent(.cancelled)
|
||||
}
|
||||
|
||||
public func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController) {
|
||||
self.plugin.onFilePickerEvent(.cancelled)
|
||||
}
|
||||
|
||||
public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
self.plugin.onFilePickerEvent(.cancelled)
|
||||
}
|
||||
|
||||
public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
|
||||
dismissViewController(picker) {
|
||||
if let url = info[.mediaURL] as? URL {
|
||||
do {
|
||||
let temporaryUrl = try self.saveTemporaryFile(url)
|
||||
self.plugin.onFilePickerEvent(.selected([temporaryUrl]))
|
||||
} catch {
|
||||
self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file"))
|
||||
}
|
||||
} else {
|
||||
self.plugin.onFilePickerEvent(.cancelled)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
extension FilePickerController: PHPickerViewControllerDelegate {
|
||||
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
|
||||
dismissViewController(picker)
|
||||
if results.first == nil {
|
||||
self.plugin.onFilePickerEvent(.cancelled)
|
||||
return
|
||||
}
|
||||
var temporaryUrls: [URL] = []
|
||||
var errorMessage: String?
|
||||
let dispatchGroup = DispatchGroup()
|
||||
for result in results {
|
||||
if errorMessage != nil {
|
||||
break
|
||||
}
|
||||
if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.movie.identifier) {
|
||||
dispatchGroup.enter()
|
||||
result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier, completionHandler: { url, error in
|
||||
defer {
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
if let error = error {
|
||||
errorMessage = error.localizedDescription
|
||||
return
|
||||
}
|
||||
guard let url = url else {
|
||||
errorMessage = "Unknown error"
|
||||
return
|
||||
}
|
||||
do {
|
||||
// We have to make a copy of the file to the app sandbox here, as PHPicker returns an NSItemProvider with either an ephemeral file URL or content that is deleted after the completion handler.
|
||||
// This is a different behavior from UIDocumentPicker, where the file can either be copied to the app sandbox or opened in place, and then accessed with `startAccessingSecurityScopedResource`.
|
||||
let temporaryUrl = try self.saveTemporaryFile(url)
|
||||
temporaryUrls.append(temporaryUrl)
|
||||
} catch {
|
||||
errorMessage = "Failed to create a temporary copy of the file"
|
||||
}
|
||||
})
|
||||
} else if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
||||
dispatchGroup.enter()
|
||||
result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier, completionHandler: { url, error in
|
||||
defer {
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
if let error = error {
|
||||
errorMessage = error.localizedDescription
|
||||
return
|
||||
}
|
||||
guard let url = url else {
|
||||
errorMessage = "Unknown error"
|
||||
return
|
||||
}
|
||||
do {
|
||||
// We have to make a copy of the file to the app sandbox here, as PHPicker returns an NSItemProvider with either an ephemeral file URL or content that is deleted after the completion handler.
|
||||
// This is a different behavior from UIDocumentPicker, where the file can either be copied to the app sandbox or opened in place, and then accessed with `startAccessingSecurityScopedResource`.
|
||||
let temporaryUrl = try self.saveTemporaryFile(url)
|
||||
temporaryUrls.append(temporaryUrl)
|
||||
} catch {
|
||||
errorMessage = "Failed to create a temporary copy of the file"
|
||||
}
|
||||
})
|
||||
} else {
|
||||
errorMessage = "Unsupported file type identifier"
|
||||
}
|
||||
}
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
if let errorMessage = errorMessage {
|
||||
self.plugin.onFilePickerEvent(.error(errorMessage))
|
||||
return
|
||||
}
|
||||
self.plugin.onFilePickerEvent(.selected(temporaryUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user