Skip to content

Commit 84209f3

Browse files
committed
fix(dialog): do not create file copy for save file picker on iOS
On iOS the file picker returns a security scoped resource file path on the save() file picker: https://developer.apple.com/documentation/uikit/uidocumentpickerviewcontroller?language=objc#Work-with-external-documents this means we can't directly access it without calling [startAccessingSecurityScopedResource](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc) this PR changes the plugin to not access the save file early, leaving that to the user and returning its actual path
1 parent 38deef4 commit 84209f3

File tree

5 files changed

+72
-53
lines changed

5 files changed

+72
-53
lines changed

examples/api/src-tauri/gen/apple/api.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@
387387
CODE_SIGN_ENTITLEMENTS = api_iOS/api_iOS.entitlements;
388388
CODE_SIGN_IDENTITY = "iPhone Developer";
389389
CODE_SIGN_STYLE = Automatic;
390-
DEVELOPMENT_TEAM = Q93MBH6S2F;
390+
DEVELOPMENT_TEAM = "Q93MBH6S2F";
391391
ENABLE_BITCODE = NO;
392392
"EXCLUDED_ARCHS[sdk=iphoneos*]" = "arm64-sim x86_64";
393393
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;
@@ -442,7 +442,7 @@
442442
CODE_SIGN_ENTITLEMENTS = api_iOS/api_iOS.entitlements;
443443
CODE_SIGN_IDENTITY = "iPhone Developer";
444444
CODE_SIGN_STYLE = Automatic;
445-
DEVELOPMENT_TEAM = Q93MBH6S2F;
445+
DEVELOPMENT_TEAM = "Q93MBH6S2F";
446446
ENABLE_BITCODE = NO;
447447
"EXCLUDED_ARCHS[sdk=iphoneos*]" = "arm64-sim x86_64";
448448
"EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64;

plugins/dialog/guest-js/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ type OpenDialogReturn<T extends OpenDialogOptions> = T['directory'] extends true
156156
* }
157157
* ```
158158
*
159+
* ## Platform-specific
160+
*
161+
* - **iOS**: Returns a copy of the file to bypass [security scoped resource](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc).
162+
*
159163
* @returns A promise resolving to the selected path(s)
160164
*
161165
* @since 2.0.0
@@ -190,6 +194,10 @@ async function open<T extends OpenDialogOptions>(
190194
* });
191195
* ```
192196
*
197+
* #### Platform-specific
198+
*
199+
* - **iOS**: Returns a copy of the file to bypass [security scoped resource](https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc).
200+
*
193201
* @returns A promise resolving to the selected path.
194202
*
195203
* @since 2.0.0

plugins/dialog/ios/Sources/DialogPlugin.swift

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,18 @@ class DialogPlugin: Plugin {
7474
onFilePickerResult = { (event: FilePickerEvent) -> Void in
7575
switch event {
7676
case .selected(let urls):
77-
invoke.resolve(["files": urls])
77+
do {
78+
let temporaryUrls = try urls.map { try self.saveTemporaryFile($0) }
79+
invoke.resolve(["files": temporaryUrls])
80+
} catch {
81+
let message = "Failed to create a temporary copy of the file: \(error)"
82+
Logger.error("\(message)")
83+
invoke.reject(message)
84+
}
7885
case .cancelled:
7986
invoke.resolve(["files": nil])
8087
case .error(let error):
88+
Logger.error("failed to pick file: \(error)")
8189
invoke.reject(error)
8290
}
8391
}
@@ -153,6 +161,7 @@ class DialogPlugin: Plugin {
153161
case .cancelled:
154162
invoke.resolve(["file": nil])
155163
case .error(let error):
164+
Logger.error("failed to pick file to save: \(error)")
156165
invoke.reject(error)
157166
}
158167
}
@@ -192,6 +201,27 @@ class DialogPlugin: Plugin {
192201
self.onFilePickerResult?(event)
193202
}
194203

204+
private func saveTemporaryFile(_ sourceUrl: URL) throws -> URL {
205+
var directory = URL(fileURLWithPath: NSTemporaryDirectory())
206+
if let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)
207+
.first
208+
{
209+
directory = cachesDirectory
210+
}
211+
let targetUrl = directory.appendingPathComponent(sourceUrl.lastPathComponent)
212+
do {
213+
try deleteFile(targetUrl)
214+
}
215+
try FileManager.default.copyItem(at: sourceUrl, to: targetUrl)
216+
return targetUrl
217+
}
218+
219+
private func deleteFile(_ url: URL) throws {
220+
if FileManager.default.fileExists(atPath: url.path) {
221+
try FileManager.default.removeItem(atPath: url.path)
222+
}
223+
}
224+
195225
@objc public func showMessageDialog(_ invoke: Invoke) throws {
196226
let manager = self.manager
197227
let args = try invoke.parseArgs(MessageDialogOptions.self)
@@ -206,8 +236,6 @@ class DialogPlugin: Plugin {
206236
UIAlertAction(
207237
title: cancelButtonLabel, style: UIAlertAction.Style.default,
208238
handler: { (_) -> Void in
209-
Logger.error("cancel")
210-
211239
invoke.resolve([
212240
"value": false,
213241
"cancelled": false,
@@ -221,8 +249,6 @@ class DialogPlugin: Plugin {
221249
UIAlertAction(
222250
title: okButtonLabel, style: UIAlertAction.Style.default,
223251
handler: { (_) -> Void in
224-
Logger.error("ok")
225-
226252
invoke.resolve([
227253
"value": true,
228254
"cancelled": false,

plugins/dialog/ios/Sources/FilePickerController.swift

Lines changed: 7 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -95,35 +95,11 @@ public class FilePickerController: NSObject {
9595
return nil
9696
}
9797
}
98-
99-
private func saveTemporaryFile(_ sourceUrl: URL) throws -> URL {
100-
var directory = URL(fileURLWithPath: NSTemporaryDirectory())
101-
if let cachesDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
102-
directory = cachesDirectory
103-
}
104-
let targetUrl = directory.appendingPathComponent(sourceUrl.lastPathComponent)
105-
do {
106-
try deleteFile(targetUrl)
107-
}
108-
try FileManager.default.copyItem(at: sourceUrl, to: targetUrl)
109-
return targetUrl
110-
}
111-
112-
private func deleteFile(_ url: URL) throws {
113-
if FileManager.default.fileExists(atPath: url.path) {
114-
try FileManager.default.removeItem(atPath: url.path)
115-
}
116-
}
11798
}
11899

119100
extension FilePickerController: UIDocumentPickerDelegate {
120101
public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
121-
do {
122-
let temporaryUrls = try urls.map { try saveTemporaryFile($0) }
123-
self.plugin.onFilePickerEvent(.selected(temporaryUrls))
124-
} catch {
125-
self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file"))
126-
}
102+
self.plugin.onFilePickerEvent(.selected(urls))
127103
}
128104

129105
public func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) {
@@ -148,12 +124,7 @@ extension FilePickerController: UIImagePickerControllerDelegate, UINavigationCon
148124
public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
149125
dismissViewController(picker) {
150126
if let url = info[.mediaURL] as? URL {
151-
do {
152-
let temporaryUrl = try self.saveTemporaryFile(url)
153-
self.plugin.onFilePickerEvent(.selected([temporaryUrl]))
154-
} catch {
155-
self.plugin.onFilePickerEvent(.error("Failed to create a temporary copy of the file"))
156-
}
127+
self.plugin.onFilePickerEvent(.selected([url]))
157128
} else {
158129
self.plugin.onFilePickerEvent(.cancelled)
159130
}
@@ -169,7 +140,7 @@ extension FilePickerController: PHPickerViewControllerDelegate {
169140
self.plugin.onFilePickerEvent(.cancelled)
170141
return
171142
}
172-
var temporaryUrls: [URL] = []
143+
var urls: [URL] = []
173144
var errorMessage: String?
174145
let dispatchGroup = DispatchGroup()
175146
for result in results {
@@ -190,12 +161,7 @@ extension FilePickerController: PHPickerViewControllerDelegate {
190161
errorMessage = "Unknown error"
191162
return
192163
}
193-
do {
194-
let temporaryUrl = try self.saveTemporaryFile(url)
195-
temporaryUrls.append(temporaryUrl)
196-
} catch {
197-
errorMessage = "Failed to create a temporary copy of the file"
198-
}
164+
urls.append(url)
199165
})
200166
} else if result.itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
201167
dispatchGroup.enter()
@@ -211,12 +177,7 @@ extension FilePickerController: PHPickerViewControllerDelegate {
211177
errorMessage = "Unknown error"
212178
return
213179
}
214-
do {
215-
let temporaryUrl = try self.saveTemporaryFile(url)
216-
temporaryUrls.append(temporaryUrl)
217-
} catch {
218-
errorMessage = "Failed to create a temporary copy of the file"
219-
}
180+
urls.append(url)
220181
})
221182
} else {
222183
errorMessage = "Unsupported file type identifier"
@@ -227,7 +188,7 @@ extension FilePickerController: PHPickerViewControllerDelegate {
227188
self.plugin.onFilePickerEvent(.error(errorMessage))
228189
return
229190
}
230-
self.plugin.onFilePickerEvent(.selected(temporaryUrls))
191+
self.plugin.onFilePickerEvent(.selected(urls))
231192
}
232193
}
233-
}
194+
}

plugins/dialog/src/lib.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,12 @@ impl<R: Runtime> FileDialogBuilder<R> {
440440
/// Ok(())
441441
/// });
442442
/// ```
443+
///
444+
/// ## Platform-specific
445+
///
446+
/// - **iOS**: Returns a copy of the file to bypass [security scoped resource].
447+
///
448+
/// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc
443449
pub fn pick_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
444450
pick_file(self, f)
445451
}
@@ -551,6 +557,12 @@ impl<R: Runtime> FileDialogBuilder<R> {
551557
/// Ok(())
552558
/// });
553559
/// ```
560+
///
561+
/// ## Platform-specific
562+
///
563+
/// - **iOS**: Returns a [security scoped resource] so you must request access before reading or writing to the file.
564+
///
565+
/// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc
554566
pub fn save_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
555567
save_file(self, f)
556568
}
@@ -573,6 +585,12 @@ impl<R: Runtime> FileDialogBuilder<R> {
573585
/// // the file path is `None` if the user closed the dialog
574586
/// }
575587
/// ```
588+
///
589+
/// ## Platform-specific
590+
///
591+
/// - **iOS**: Returns a copy of the file to bypass [security scoped resource].
592+
///
593+
/// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc
576594
pub fn blocking_pick_file(self) -> Option<FilePath> {
577595
blocking_fn!(self, pick_file)
578596
}
@@ -651,6 +669,12 @@ impl<R: Runtime> FileDialogBuilder<R> {
651669
/// // the file path is `None` if the user closed the dialog
652670
/// }
653671
/// ```
672+
///
673+
/// ## Platform-specific
674+
///
675+
/// - **iOS**: Returns a [security scoped resource] so you must request access before reading or writing to the file.
676+
///
677+
/// [security scoped resource]: https://developer.apple.com/documentation/foundation/nsurl/1417051-startaccessingsecurityscopedreso?language=objc
654678
pub fn blocking_save_file(self) -> Option<FilePath> {
655679
blocking_fn!(self, save_file)
656680
}

0 commit comments

Comments
 (0)