diff --git a/.changes/dialog-ios-fix.md b/.changes/dialog-ios-fix.md new file mode 100644 index 00000000..da79ba01 --- /dev/null +++ b/.changes/dialog-ios-fix.md @@ -0,0 +1,6 @@ +--- +"dialog": patch +"dialog-js": patch +--- + +Add support for folder picker on iOS. \ No newline at end of file diff --git a/plugins/dialog/ios/Sources/DialogPlugin.swift b/plugins/dialog/ios/Sources/DialogPlugin.swift index b3f7e7da..6ebb801d 100644 --- a/plugins/dialog/ios/Sources/DialogPlugin.swift +++ b/plugins/dialog/ios/Sources/DialogPlugin.swift @@ -38,6 +38,14 @@ struct SaveFileDialogOptions: Decodable { var defaultPath: String? } +struct FolderPickerOptions: Decodable { + var title: String? + var defaultPath: String? + var multiple: Bool? + var recursive: Bool? + var canCreateDirectories: Bool? +} + class DialogPlugin: Plugin { var filePickerController: FilePickerController! @@ -168,6 +176,47 @@ class DialogPlugin: Plugin { } } + @objc public func showFolderPicker(_ invoke: Invoke) throws { + let args = try invoke.parseArgs(FolderPickerOptions.self) + + onFilePickerResult = { (event: FilePickerEvent) -> Void in + switch event { + case .selected(let urls): + invoke.resolve(["directories": urls]) + case .cancelled: + invoke.resolve(["directories": nil]) + case .error(let error): + invoke.reject(error) + } + } + + DispatchQueue.main.async { + let picker: UIDocumentPickerViewController + if #available(iOS 14.0, *) { + picker = UIDocumentPickerViewController(forOpeningContentTypes: [.folder]) + } else { + picker = UIDocumentPickerViewController(documentTypes: [kUTTypeFolder as String], in: .open) + } + + if let title = args.title { + picker.title = title + } + + if let defaultPath = args.defaultPath { + picker.directoryURL = URL(string: defaultPath) + } + + picker.delegate = self.filePickerController + picker.allowsMultipleSelection = args.multiple ?? false + + // Note: canCreateDirectories is only supported on macOS + // recursive is handled at the filesystem access level, not in the picker + + picker.modalPresentationStyle = .fullScreen + self.presentViewController(picker) + } + } + private func presentViewController(_ viewControllerToPresent: UIViewController) { self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil) } diff --git a/plugins/dialog/ios/Sources/FilePickerController.swift b/plugins/dialog/ios/Sources/FilePickerController.swift index b2752f0b..4da7c46f 100644 --- a/plugins/dialog/ios/Sources/FilePickerController.swift +++ b/plugins/dialog/ios/Sources/FilePickerController.swift @@ -7,6 +7,7 @@ import MobileCoreServices import PhotosUI import Photos import Tauri +import UniformTypeIdentifiers public class FilePickerController: NSObject { var plugin: DialogPlugin @@ -118,6 +119,15 @@ public class FilePickerController: NSObject { extension FilePickerController: UIDocumentPickerDelegate { public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + // Check if this is a folder picker by examining the URLs + let isFolder = urls.first?.hasDirectoryPath ?? false + + if isFolder { + self.plugin.onFilePickerEvent(.selected(urls)) + return + } + + // Handle regular files do { let temporaryUrls = try urls.map { try saveTemporaryFile($0) } self.plugin.onFilePickerEvent(.selected(temporaryUrls)) diff --git a/plugins/dialog/src/commands.rs b/plugins/dialog/src/commands.rs index c3caf027..915fd711 100644 --- a/plugins/dialog/src/commands.rs +++ b/plugins/dialog/src/commands.rs @@ -16,9 +16,9 @@ use crate::{ #[derive(Serialize)] #[serde(untagged)] pub enum OpenResponse { - #[cfg(desktop)] + #[cfg(any(desktop, target_os = "ios"))] Folders(Option>), - #[cfg(desktop)] + #[cfg(any(desktop, target_os = "ios"))] Folder(Option), Files(Option>), File(Option), @@ -133,7 +133,7 @@ pub(crate) async fn open( } let res = if options.directory { - #[cfg(desktop)] + #[cfg(any(desktop, target_os = "ios"))] { let tauri_scope = window.state::(); @@ -165,7 +165,7 @@ pub(crate) async fn open( OpenResponse::Folder(folder.map(|p| p.simplified())) } } - #[cfg(mobile)] + #[cfg(all(mobile, not(target_os = "ios")))] return Err(crate::Error::FolderPickerNotImplemented); } else if options.multiple { let tauri_scope = window.state::(); diff --git a/plugins/dialog/src/lib.rs b/plugins/dialog/src/lib.rs index 2ef1c1ea..5f2aa49a 100644 --- a/plugins/dialog/src/lib.rs +++ b/plugins/dialog/src/lib.rs @@ -506,7 +506,7 @@ impl FileDialogBuilder { /// Ok(()) /// }); /// ``` - #[cfg(desktop)] + #[cfg(any(desktop, target_os = "ios"))] pub fn pick_folder) + Send + 'static>(self, f: F) { pick_folder(self, f) } @@ -528,7 +528,7 @@ impl FileDialogBuilder { /// Ok(()) /// }); /// ``` - #[cfg(desktop)] + #[cfg(any(desktop, target_os = "ios"))] pub fn pick_folders>) + Send + 'static>(self, f: F) { pick_folders(self, f) } @@ -611,7 +611,7 @@ impl FileDialogBuilder { /// // the folder path is `None` if the user closed the dialog /// } /// ``` - #[cfg(desktop)] + #[cfg(any(desktop, target_os = "ios"))] pub fn blocking_pick_folder(self) -> Option { blocking_fn!(self, pick_folder) } @@ -631,7 +631,7 @@ impl FileDialogBuilder { /// // the folder paths value is `None` if the user closed the dialog /// } /// ``` - #[cfg(desktop)] + #[cfg(any(desktop, target_os = "ios"))] pub fn blocking_pick_folders(self) -> Option> { blocking_fn!(self, pick_folders) } diff --git a/plugins/dialog/src/mobile.rs b/plugins/dialog/src/mobile.rs index b73def4f..a2e4e29c 100644 --- a/plugins/dialog/src/mobile.rs +++ b/plugins/dialog/src/mobile.rs @@ -54,6 +54,11 @@ struct SaveFileResponse { file: FilePath, } +#[derive(Debug, Deserialize)] +struct FolderPickerResponse { + directories: Vec, +} + pub fn pick_file) + Send + 'static>( dialog: FileDialogBuilder, f: F, @@ -125,3 +130,39 @@ pub fn show_message_dialog( f(res.map(|r| r.value).unwrap_or_default()) }); } + +#[cfg(target_os = "ios")] +pub fn pick_folders>) + Send + 'static>( + dialog: FileDialogBuilder, + f: F, +) { + std::thread::spawn(move || { + let res = dialog + .dialog + .0 + .run_mobile_plugin::("showFolderPicker", dialog.payload(true)); + if let Ok(response) = res { + f(Some(response.directories)) + } else { + f(None) + } + }); +} + +#[cfg(target_os = "ios")] +pub fn pick_folder) + Send + 'static>( + dialog: FileDialogBuilder, + f: F, +) { + std::thread::spawn(move || { + let res = dialog + .dialog + .0 + .run_mobile_plugin::("showFolderPicker", dialog.payload(false)); + if let Ok(response) = res { + f(Some(response.directories.into_iter().next().unwrap())) + } else { + f(None) + } + }); +}