diff --git a/examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj b/examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj index 64ef20a865..798ed66afb 100644 --- a/examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj +++ b/examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj @@ -387,7 +387,7 @@ CODE_SIGN_ENTITLEMENTS = api_iOS/api_iOS.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = Q93MBH6S2F; + DEVELOPMENT_TEAM = "Q93MBH6S2F"; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphoneos*]" = "arm64-sim x86_64"; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -442,7 +442,7 @@ CODE_SIGN_ENTITLEMENTS = api_iOS/api_iOS.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = Q93MBH6S2F; + DEVELOPMENT_TEAM = "Q93MBH6S2F"; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphoneos*]" = "arm64-sim x86_64"; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; diff --git a/plugins/dialog/guest-js/index.ts b/plugins/dialog/guest-js/index.ts index 150be95a00..7a30ca3c28 100644 --- a/plugins/dialog/guest-js/index.ts +++ b/plugins/dialog/guest-js/index.ts @@ -156,6 +156,10 @@ type OpenDialogReturn = T['directory'] extends true * } * ``` * + * ## Platform-specific + * + * - **iOS**: Returns a copy of the file to bypass [security scoped resource](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc). + * * @returns A promise resolving to the selected path(s) * * @since 2.0.0 @@ -190,6 +194,10 @@ async function open( * }); * ``` * + * #### Platform-specific + * + * - **iOS**: Returns a copy of the file to bypass [security scoped resource](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc). + * * @returns A promise resolving to the selected path. * * @since 2.0.0 diff --git a/plugins/dialog/ios/Sources/DialogPlugin.swift b/plugins/dialog/ios/Sources/DialogPlugin.swift index b3f7e7da6e..8e8cd71530 100644 --- a/plugins/dialog/ios/Sources/DialogPlugin.swift +++ b/plugins/dialog/ios/Sources/DialogPlugin.swift @@ -74,10 +74,18 @@ class DialogPlugin: Plugin { onFilePickerResult = { (event: FilePickerEvent) -> Void in switch event { case .selected(let urls): - invoke.resolve(["files": urls]) + do { + let temporaryUrls = try urls.map { try self.saveTemporaryFile($0) } + invoke.resolve(["files": temporaryUrls]) + } catch { + let message = "Failed to create a temporary copy of the file: \(error)" + Logger.error("\(message)") + invoke.reject(message) + } case .cancelled: invoke.resolve(["files": nil]) case .error(let error): + Logger.error("failed to pick file: \(error)") invoke.reject(error) } } @@ -149,10 +157,12 @@ class DialogPlugin: Plugin { onFilePickerResult = { (event: FilePickerEvent) -> Void in switch event { case .selected(let urls): + urls.first?.startAccessingSecurityScopedResource() invoke.resolve(["file": urls.first!]) case .cancelled: invoke.resolve(["file": nil]) case .error(let error): + Logger.error("failed to pick file to save: \(error)") invoke.reject(error) } } @@ -192,6 +202,27 @@ class DialogPlugin: Plugin { self.onFilePickerResult?(event) } + 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) + } + } + @objc public func showMessageDialog(_ invoke: Invoke) throws { let manager = self.manager let args = try invoke.parseArgs(MessageDialogOptions.self) @@ -206,8 +237,6 @@ class DialogPlugin: Plugin { UIAlertAction( title: cancelButtonLabel, style: UIAlertAction.Style.default, handler: { (_) -> Void in - Logger.error("cancel") - invoke.resolve([ "value": false, "cancelled": false, @@ -221,8 +250,6 @@ class DialogPlugin: Plugin { UIAlertAction( title: okButtonLabel, style: UIAlertAction.Style.default, handler: { (_) -> Void in - Logger.error("ok") - invoke.resolve([ "value": true, "cancelled": false, diff --git a/plugins/dialog/ios/Sources/FilePickerController.swift b/plugins/dialog/ios/Sources/FilePickerController.swift index b2752f0b03..20c129d804 100644 --- a/plugins/dialog/ios/Sources/FilePickerController.swift +++ b/plugins/dialog/ios/Sources/FilePickerController.swift @@ -95,35 +95,11 @@ public class FilePickerController: NSObject { return nil } } - - 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 { - let temporaryUrls = try urls.map { try saveTemporaryFile($0) } - self.plugin.onFilePickerEvent(.selected(temporaryUrls)) - } catch { - self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file")) - } + self.plugin.onFilePickerEvent(.selected(urls)) } public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { @@ -148,12 +124,7 @@ extension FilePickerController: UIImagePickerControllerDelegate, UINavigationCon 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")) - } + self.plugin.onFilePickerEvent(.selected([url])) } else { self.plugin.onFilePickerEvent(.cancelled) } @@ -169,7 +140,7 @@ extension FilePickerController: PHPickerViewControllerDelegate { self.plugin.onFilePickerEvent(.cancelled) return } - var temporaryUrls: [URL] = [] + var urls: [URL] = [] var errorMessage: String? let dispatchGroup = DispatchGroup() for result in results { @@ -190,12 +161,7 @@ extension FilePickerController: PHPickerViewControllerDelegate { errorMessage = "Unknown error" return } - do { - let temporaryUrl = try self.saveTemporaryFile(url) - temporaryUrls.append(temporaryUrl) - } catch { - errorMessage = "Failed to create a temporary copy of the file" - } + urls.append(url) }) } else if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { dispatchGroup.enter() @@ -211,12 +177,7 @@ extension FilePickerController: PHPickerViewControllerDelegate { errorMessage = "Unknown error" return } - do { - let temporaryUrl = try self.saveTemporaryFile(url) - temporaryUrls.append(temporaryUrl) - } catch { - errorMessage = "Failed to create a temporary copy of the file" - } + urls.append(url) }) } else { errorMessage = "Unsupported file type identifier" @@ -227,7 +188,7 @@ extension FilePickerController: PHPickerViewControllerDelegate { self.plugin.onFilePickerEvent(.error(errorMessage)) return } - self.plugin.onFilePickerEvent(.selected(temporaryUrls)) + self.plugin.onFilePickerEvent(.selected(urls)) } } -} \ No newline at end of file +} diff --git a/plugins/dialog/src/lib.rs b/plugins/dialog/src/lib.rs index 2ef1c1eade..d6c4d19f76 100644 --- a/plugins/dialog/src/lib.rs +++ b/plugins/dialog/src/lib.rs @@ -440,6 +440,12 @@ impl FileDialogBuilder { /// Ok(()) /// }); /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS**: Returns a copy of the file to bypass [security scoped resource]. + /// + /// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc pub fn pick_file) + Send + 'static>(self, f: F) { pick_file(self, f) } @@ -551,6 +557,12 @@ impl FileDialogBuilder { /// Ok(()) /// }); /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS**: Returns a [security scoped resource] so you must request access before reading or writing to the file. + /// + /// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc pub fn save_file) + Send + 'static>(self, f: F) { save_file(self, f) } @@ -573,6 +585,12 @@ impl FileDialogBuilder { /// // the file path is `None` if the user closed the dialog /// } /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS**: Returns a copy of the file to bypass [security scoped resource]. + /// + /// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc pub fn blocking_pick_file(self) -> Option { blocking_fn!(self, pick_file) } @@ -651,6 +669,12 @@ impl FileDialogBuilder { /// // the file path is `None` if the user closed the dialog /// } /// ``` + /// + /// ## Platform-specific + /// + /// - **iOS**: Returns a [security scoped resource] so you must request access before reading or writing to the file. + /// + /// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc pub fn blocking_save_file(self) -> Option { blocking_fn!(self, save_file) }