feat(dialog): implement save API on iOS (#1707)

pull/1531/merge
Lucas Fernandes Nogueira 9 months ago committed by GitHub
parent ff134a8ca4
commit feb1e93fcb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"dialog": patch:feat
---
Implement `save` API on iOS.

@ -17,10 +17,10 @@ enum FilePickerEvent {
} }
struct MessageDialogOptions: Decodable { struct MessageDialogOptions: Decodable {
let title: String? var title: String?
let message: String let message: String
let okButtonLabel: String? var okButtonLabel: String?
let cancelButtonLabel: String? var cancelButtonLabel: String?
} }
struct Filter: Decodable { struct Filter: Decodable {
@ -30,13 +30,18 @@ struct Filter: Decodable {
struct FilePickerOptions: Decodable { struct FilePickerOptions: Decodable {
var multiple: Bool? var multiple: Bool?
var filters: [Filter]? var filters: [Filter]?
var defaultPath: String?
}
struct SaveFileDialogOptions: Decodable {
var fileName: String?
var defaultPath: String?
} }
class DialogPlugin: Plugin { class DialogPlugin: Plugin {
var filePickerController: FilePickerController! var filePickerController: FilePickerController!
var pendingInvoke: Invoke? = nil var onFilePickerResult: ((FilePickerEvent) -> Void)? = nil
var pendingInvokeArgs: FilePickerOptions? = nil
override init() { override init() {
super.init() super.init()
@ -66,8 +71,16 @@ class DialogPlugin: Plugin {
} }
} }
pendingInvoke = invoke onFilePickerResult = { (event: FilePickerEvent) -> Void in
pendingInvokeArgs = args switch event {
case .selected(let urls):
invoke.resolve(["files": urls])
case .cancelled:
invoke.resolve(["files": nil])
case .error(let error):
invoke.reject(error)
}
}
if uniqueMimeType == true || isMedia { if uniqueMimeType == true || isMedia {
DispatchQueue.main.async { DispatchQueue.main.async {
@ -104,6 +117,9 @@ class DialogPlugin: Plugin {
let documentTypes = parsedTypes.isEmpty ? ["public.data"] : parsedTypes let documentTypes = parsedTypes.isEmpty ? ["public.data"] : parsedTypes
DispatchQueue.main.async { DispatchQueue.main.async {
let picker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import) let picker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .import)
if let defaultPath = args.defaultPath {
picker.directoryURL = URL(string: defaultPath)
}
picker.delegate = self.filePickerController picker.delegate = self.filePickerController
picker.allowsMultipleSelection = args.multiple ?? false picker.allowsMultipleSelection = args.multiple ?? false
picker.modalPresentationStyle = .fullScreen picker.modalPresentationStyle = .fullScreen
@ -112,6 +128,46 @@ class DialogPlugin: Plugin {
} }
} }
@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) { private func presentViewController(_ viewControllerToPresent: UIViewController) {
self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil) self.manager.viewController?.present(viewControllerToPresent, animated: true, completion: nil)
} }
@ -133,14 +189,7 @@ class DialogPlugin: Plugin {
} }
public func onFilePickerEvent(_ event: FilePickerEvent) { public func onFilePickerEvent(_ event: FilePickerEvent) {
switch event { self.onFilePickerResult?(event)
case .selected(let urls):
pendingInvoke?.resolve(["files": urls])
case .cancelled:
pendingInvoke?.resolve(["files": nil])
case .error(let error):
pendingInvoke?.reject(error)
}
} }
@objc public func showMessageDialog(_ invoke: Invoke) throws { @objc public func showMessageDialog(_ invoke: Invoke) throws {

@ -197,10 +197,6 @@ pub(crate) async fn save<R: Runtime>(
dialog: State<'_, Dialog<R>>, dialog: State<'_, Dialog<R>>,
options: SaveDialogOptions, options: SaveDialogOptions,
) -> Result<Option<FilePath>> { ) -> Result<Option<FilePath>> {
#[cfg(target_os = "ios")]
return Err(crate::Error::FileSaveDialogNotImplemented);
#[cfg(any(desktop, target_os = "android"))]
{
let mut dialog_builder = dialog.file(); let mut dialog_builder = dialog.file();
#[cfg(any(windows, target_os = "macos"))] #[cfg(any(windows, target_os = "macos"))]
{ {
@ -231,7 +227,6 @@ pub(crate) async fn save<R: Runtime>(
} }
Ok(path.map(|p| p.simplified())) Ok(path.map(|p| p.simplified()))
}
} }
fn message_dialog<R: Runtime>( fn message_dialog<R: Runtime>(

@ -18,9 +18,6 @@ pub enum Error {
#[cfg(mobile)] #[cfg(mobile)]
#[error("Folder picker is not implemented on mobile")] #[error("Folder picker is not implemented on mobile")]
FolderPickerNotImplemented, FolderPickerNotImplemented,
#[cfg(target_os = "ios")]
#[error("File save dialog is not implemented on iOS")]
FileSaveDialogNotImplemented,
#[error(transparent)] #[error(transparent)]
Fs(#[from] tauri_plugin_fs::Error), Fs(#[from] tauri_plugin_fs::Error),
#[error("URL is not a valid path")] #[error("URL is not a valid path")]

Loading…
Cancel
Save