Skip to content

Commit ed0782f

Browse files
authored
feat(ios): support swift 6 concurrency (#908)
* feat(ios): support swift 6 concurrency * Update picker package version to major
1 parent f2be0db commit ed0782f

19 files changed

+338
-285
lines changed

.changeset/spotty-queens-smash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@react-native-documents/picker": major
3+
---
4+
5+
feat(ios): support swift 6 concurrency

packages/document-picker/ios/RNDocumentPicker.mm

Lines changed: 19 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,23 @@ @interface RNDocumentPicker ()
1818
@end
1919

2020
@implementation RNDocumentPicker {
21-
DocPicker *docPicker;
22-
DocSaver *docSaver;
21+
DocPicker *_docPicker;
22+
DocSaver *_docSaver;
2323
}
2424

25-
- (instancetype)init {
26-
if ((self = [super init])) {
27-
docPicker = [DocPicker new];
28-
docSaver = [DocSaver new];
25+
// initialization happens on serial queue so there are no races
26+
- (DocPicker *)docPicker {
27+
if (!_docPicker) {
28+
_docPicker = [DocPicker new];
2929
}
30-
return self;
30+
return _docPicker;
3131
}
3232

33-
+ (BOOL)requiresMainQueueSetup {
34-
return NO;
33+
- (DocSaver *)docSaver {
34+
if (!_docSaver) {
35+
_docSaver = [DocSaver new];
36+
}
37+
return _docSaver;
3538
}
3639

3740
RCT_EXPORT_MODULE()
@@ -43,26 +46,9 @@ + (BOOL)requiresMainQueueSetup {
4346
reject:
4447
(RCTPromiseRejectBlock) reject)
4548
{
46-
// https://stackoverflow.com/questions/5270519/what-is-difference-between-uimodaltransitionstyle-and-uimodalpresentationstyle
47-
UIModalPresentationStyle presentationStyle = [RCTConvert UIModalPresentationStyle:options[@"presentationStyle"]];
48-
UIModalTransitionStyle transitionStyle = [RCTConvert UIModalTransitionStyle:options[@"transitionStyle"]];
49-
NSArray *allowedUTIs = [RCTConvert NSArray:options[@"type"]];
50-
BOOL allowMultiple = [RCTConvert BOOL:options[@"allowMultiSelection"]];
51-
BOOL showExtensions = [RCTConvert BOOL:options[@"showFileExtensions"]];
52-
NSString *mode = options[@"mode"];
53-
NSString *initialDir = options[@"initialDirectoryUrl"];
54-
BOOL requestLongTermAccess = [RCTConvert BOOL:options[@"requestLongTermAccess"]];
55-
56-
PickerOptions *pickerOptions = [[PickerOptions alloc] initWithTypes:allowedUTIs
57-
mode:mode
58-
initialDirectoryUrl:initialDir
59-
allowMultiSelection:allowMultiple
60-
shouldShowFileExtensions:showExtensions
61-
transitionStyle:transitionStyle
62-
presentationStyle:presentationStyle
63-
requestLongTermAccess:requestLongTermAccess];
64-
65-
[docPicker presentWithOptions:pickerOptions resolve:resolve reject:reject];
49+
dispatch_async(dispatch_get_main_queue(), ^{
50+
[self.docPicker presentWithOptionsDict:options resolve:resolve reject:reject];
51+
});
6652
}
6753

6854
RCT_EXPORT_METHOD(pickDirectory:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
@@ -92,21 +78,13 @@ + (BOOL)requiresMainQueueSetup {
9278
}
9379

9480
RCT_EXPORT_METHOD(writeDocuments:(NSDictionary *)options resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
95-
UIModalPresentationStyle presentationStyle = [RCTConvert UIModalPresentationStyle:options[@"presentationStyle"]];
96-
UIModalTransitionStyle transitionStyle = [RCTConvert UIModalTransitionStyle:options[@"transitionStyle"]];
97-
BOOL showExtensions = [RCTConvert BOOL:options[@"showFileExtensions"]];
98-
BOOL asCopy = [RCTConvert BOOL:options[@"copy"]];
99-
100-
NSString *initialDir = options[@"initialDirectoryUri"];
101-
NSArray<NSString*> *documentUrl = options[@"sourceUris"];
102-
103-
SaverOptions* saverOptions = [[SaverOptions alloc] initWithSourceUrlStrings:documentUrl asCopy:asCopy initialDirectoryUrl:initialDir shouldShowFileExtensions:showExtensions transitionStyle:transitionStyle presentationStyle:presentationStyle];
104-
105-
[docSaver presentWithOptions:saverOptions resolve:resolve reject:reject];
81+
dispatch_async(dispatch_get_main_queue(), ^{
82+
[self.docSaver presentWithOptionsDict:options resolve:resolve reject:reject];
83+
});
10684
}
10785

10886
RCT_EXPORT_METHOD(releaseSecureAccess:(NSArray *)uris resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject) {
109-
[docPicker stopAccessingOpenedUrls:uris];
87+
[self.docPicker stopAccessingOpenedUrls:uris];
11088
resolve([NSNull null]);
11189
}
11290

packages/document-picker/ios/swift/DocPicker.swift

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,48 @@
22

33
import Foundation
44
import UniformTypeIdentifiers
5-
import MobileCoreServices
65

7-
@objc public class DocPicker: PickerWithMetadataImpl {
6+
@objc public class DocPicker: PickerBase {
7+
var pickerOptions: PickerOptions?
8+
var openedUrls: Set<URL> = []
89

9-
var currentOptions: PickerOptions? = nil
10+
@MainActor
11+
override func createDocumentPicker(from dictionary: NSDictionary) -> UIDocumentPickerViewController {
12+
let options = PickerOptions(dictionary: dictionary)
13+
self.pickerOptions = options
14+
return options.createDocumentPicker()
15+
}
1016

11-
@objc public func present(options: PickerOptions, resolve: @escaping RNDPPromiseResolveBlock, reject: @escaping RNDPPromiseRejectBlock) {
12-
// TODO fix callsite param
13-
if (!promiseWrapper.trySetPromiseRejectingIncoming(resolve, rejecter: reject, fromCallSite: "pick")) {
14-
return;
15-
}
16-
currentOptions = options;
17-
DispatchQueue.main.async {
18-
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: options.allowedTypes, asCopy: options.modeAsCopy())
17+
public override func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
18+
guard let promise = promiseWrapper.takeCallbacks() else { return }
19+
let options = self.pickerOptions
1920

20-
documentPicker.modalPresentationStyle = options.presentationStyle
21-
documentPicker.allowsMultipleSelection = options.allowMultiSelection
22-
documentPicker.modalTransitionStyle = options.transitionStyle
23-
// documentPicker.directoryURL = options.initialDirectoryUrl
24-
// documentPicker.shouldShowFileExtensions = options.shouldShowFileExtensions
21+
Task.detached(priority: .userInitiated) {
22+
let documentsInfo = self.createDocumentMetadataWithOptions(for: urls, options: options)
23+
.compactMap { $0.build() }
24+
promise.resolve(documentsInfo)
25+
}
26+
}
2527

26-
self.presentInternal(documentPicker: documentPicker)
28+
nonisolated private func createDocumentMetadataWithOptions(for urls: [URL], options: PickerOptions?) -> [DocumentMetadataBuilder] {
29+
return urls.compactMap { url in
30+
do {
31+
return try self.getMetadataForWithOptions(url: url, options: options)
32+
} catch {
33+
return DocumentMetadataBuilder(forUri: url, error: error)
34+
}
2735
}
2836
}
2937

30-
public func getMetadataFor(url: URL) throws -> DocumentMetadataBuilder {
31-
return if (currentOptions?.isOpenMode() == true) {
32-
try self.getOpenedDocumentInfo(url: url, requestLongTermAccess: currentOptions?.requestLongTermAccess ?? false)
38+
nonisolated private func getMetadataForWithOptions(url: URL, options: PickerOptions?) throws -> DocumentMetadataBuilder {
39+
return if options?.isOpenMode() == true {
40+
try self.getOpenedDocumentInfo(url: url, requestLongTermAccess: options?.requestLongTermAccess ?? false)
3341
} else {
3442
try self.getAnyModeMetadata(url: url)
3543
}
3644
}
3745

38-
private func getAnyModeMetadata(url: URL) throws -> DocumentMetadataBuilder {
46+
nonisolated private func getAnyModeMetadata(url: URL) throws -> DocumentMetadataBuilder {
3947
let resourceValues = try url.resourceValues(forKeys: [.fileSizeKey, .nameKey, .isDirectoryKey, .contentTypeKey])
4048

4149
return DocumentMetadataBuilder(forUri: url, resourceValues: resourceValues)
@@ -45,18 +53,20 @@ import MobileCoreServices
4553
case sourceAccessError
4654
}
4755

48-
func getOpenedDocumentInfo(url: URL, requestLongTermAccess: Bool) throws -> DocumentMetadataBuilder {
56+
nonisolated private func getOpenedDocumentInfo(url: URL, requestLongTermAccess: Bool) throws -> DocumentMetadataBuilder {
4957
guard url.startAccessingSecurityScopedResource() else {
5058
throw KeepLocalCopyError.sourceAccessError
5159
}
5260

53-
// url.stopAccessingSecurityScopedResource() must be called later
54-
openedUrls.append(url)
61+
// url.stopAccessingSecurityScopedResource() must be called later by user
62+
DispatchQueue.main.async { [weak self] in
63+
self?.openedUrls.insert(url)
64+
}
5565

56-
// Use file coordination for reading and writing any of the URLs content.
66+
// Use file coordination for reading and writing any of the URL's content.
5767
var error: NSError? = nil
5868
var success = false
59-
var metadataBuilder: DocumentMetadataBuilder = DocumentMetadataBuilder(forUri: url)
69+
var metadataBuilder = DocumentMetadataBuilder(forUri: url)
6070

6171
NSFileCoordinator().coordinate(readingItemAt: url, error: &error) { (url) in
6272
do {
@@ -66,7 +76,7 @@ import MobileCoreServices
6676
metadataBuilder.setMetadataReadingError(error)
6777
}
6878

69-
if (requestLongTermAccess == true) {
79+
if requestLongTermAccess {
7080
do {
7181
let bookmarkData = try url.bookmarkData(options: .minimalBookmark, includingResourceValuesForKeys: nil, relativeTo: nil)
7282
metadataBuilder.setBookmark(bookmarkData)
@@ -75,10 +85,18 @@ import MobileCoreServices
7585
}
7686
}
7787
}
78-
if let err = error, success == false {
88+
if let err = error, !success {
7989
throw err
8090
}
8191
return metadataBuilder
8292
}
8393

94+
@objc public func stopAccessingOpenedUrls(_ urlStrings: [String]) {
95+
let incomingUrls = Set(urlStrings.compactMap { URL(string: $0) })
96+
for url in openedUrls.intersection(incomingUrls) {
97+
url.stopAccessingSecurityScopedResource()
98+
openedUrls.remove(url)
99+
}
100+
}
101+
84102
}

packages/document-picker/ios/swift/DocSaver.swift

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,28 @@
99

1010
import Foundation
1111
import UniformTypeIdentifiers
12-
import MobileCoreServices
1312

14-
@objc public class DocSaver: PickerWithMetadataImpl {
13+
@objc public class DocSaver: PickerBase {
1514

16-
@objc public func present(options: SaverOptions, resolve: @escaping (Any?) -> Void, reject: @escaping (String?, String?, Error?) -> Void) {
17-
if (!promiseWrapper.trySetPromiseRejectingIncoming(resolve, rejecter: reject, fromCallSite: "saveDocuments")) {
18-
return;
19-
}
20-
DispatchQueue.main.async {
21-
let documentPicker = UIDocumentPickerViewController(forExporting: options.sourceUrls, asCopy: options.asCopy)
22-
23-
documentPicker.modalPresentationStyle = options.presentationStyle
24-
documentPicker.modalTransitionStyle = options.transitionStyle
25-
// documentPicker.directoryURL = options.initialDirectoryUrl
26-
// documentPicker.shouldShowFileExtensions = options.shouldShowFileExtensions
27-
28-
self.presentInternal(documentPicker: documentPicker)
29-
}
15+
@MainActor
16+
override func createDocumentPicker(from dictionary: NSDictionary) -> UIDocumentPickerViewController {
17+
let options = SaverOptions(dictionary: dictionary)
18+
return options.createDocumentPicker()
3019
}
3120

32-
public func getMetadataFor(url: URL) throws -> DocumentMetadataBuilder {
33-
let name = url.lastPathComponent.removingPercentEncoding
34-
35-
var resourceValues = URLResourceValues()
36-
resourceValues.name = name
37-
38-
return DocumentMetadataBuilder(forUri: url, resourceValues: resourceValues)
21+
public override func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
22+
guard let promise = promiseWrapper.takeCallbacks() else { return }
23+
24+
Task.detached(priority: .userInitiated) {
25+
// runs off main thread - preserves I/O performance
26+
let documentsInfo = urls.compactMap { url -> [String: Any?]? in
27+
let name = url.lastPathComponent.removingPercentEncoding
28+
var resourceValues = URLResourceValues()
29+
resourceValues.name = name
30+
return DocumentMetadataBuilder(forUri: url, resourceValues: resourceValues).build()
31+
}
32+
promise.resolve(documentsInfo)
33+
}
3934
}
4035

4136
}

packages/document-picker/ios/swift/DocumentMetadataBuilder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public class DocumentMetadataBuilder {
3939

4040
func build() -> [String: Any?] {
4141
var dictionary: [String: Any?] = [:]
42-
if (resourceValues?.isDirectory ?? false == false) {
42+
if resourceValues?.isDirectory != true {
4343
let utTypeFromFile: UTType? = resourceValues?.contentType
4444
let utType: UTType? = utTypeFromFile ?? UTType(filenameExtension: uri.pathExtension)
4545

packages/document-picker/ios/swift/FileOperations.swift

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,37 @@
11
// LICENSE: see License.md in the package root
22

33
import Foundation
4+
import React
45

56
@objc public class FileOperations: NSObject {
6-
7-
@objc public static func keepLocalCopyAtUniqueDestination(from: [[String: String]], destinationPreset: String, resolve: @escaping RNDPPromiseResolveBlock) {
8-
DispatchQueue.global(qos: .utility).async {
9-
let results = moveFiles(from: from, destinationPreset: destinationPreset)
10-
resolve(results)
7+
8+
@objc public static func keepLocalCopyAtUniqueDestination(from: [[String: String]], destinationPreset: String, resolve: @escaping RCTPromiseResolveBlock) {
9+
let callback = ResolveCallback(resolve: resolve)
10+
Task.detached(priority: .userInitiated) {
11+
let results = Self.moveFiles(from: from, destinationPreset: destinationPreset)
12+
callback.resolve(results)
1113
}
1214
}
13-
14-
static func moveFiles(from: [[String: String]], destinationPreset: String) -> [[String: String?]] {
15+
16+
private static func moveFiles(from: [[String: String]], destinationPreset: String) -> [[String: String?]] {
1517
let destinationRootDir = getDirectoryForFileDestination(destinationPreset)
1618
let uniqueSubDirName = UUID().uuidString
1719
let destinationDir = destinationRootDir.appendingPathComponent(uniqueSubDirName, isDirectory: true)
18-
20+
1921
do {
2022
try FileManager.default.createDirectory(at: destinationDir, withIntermediateDirectories: true, attributes: nil)
2123
} catch {
2224
return from.map { dictionary in
2325
LocalCopyResponse.error(sourceUri: dictionary["uri"], copyError: "Failed to create destination directory: \(error.localizedDescription)").dictionaryRepresentation
2426
}
2527
}
26-
28+
2729
// move files
2830
return from.map { dictionary in
29-
moveSingleFile(dictionary: dictionary, destinationDir: destinationDir).dictionaryRepresentation
31+
Self.moveSingleFile(dictionary: dictionary, destinationDir: destinationDir).dictionaryRepresentation
3032
}
3133
}
32-
34+
3335
private static func moveSingleFile(dictionary: [String: String], destinationDir: URL) -> LocalCopyResponse {
3436
guard let uriString = dictionary["uri"],
3537
let uri = URL(string: uriString),
@@ -39,32 +41,32 @@ import Foundation
3941
copyError: "Invalid URI or fileName"
4042
)
4143
}
42-
44+
4345
do {
4446
let destinationUrl = try moveToDestination(from: uri, usingFilename: fileName, destinationDir: destinationDir)
4547
return LocalCopyResponse.success(sourceUri: uri.absoluteString, localUri: destinationUrl.absoluteString)
4648
} catch {
4749
return LocalCopyResponse.error(sourceUri: uriString, copyError: error.localizedDescription)
4850
}
4951
}
50-
51-
static func moveToDestination(from: URL, usingFilename fileName: String, destinationDir: URL) throws -> URL {
52+
53+
private static func moveToDestination(from: URL, usingFilename fileName: String, destinationDir: URL) throws -> URL {
5254
let destinationFile = destinationDir.appendingPathComponent(fileName).standardized
53-
55+
5456
guard destinationFile.path.hasPrefix(destinationDir.standardized.path) else {
5557
throw NSError(
5658
domain: "PathTraversalPrevention",
5759
code: 400,
5860
userInfo: [NSLocalizedDescriptionKey: "The copied file is attempting to write outside of the target directory."]
5961
)
6062
}
61-
63+
6264
try FileManager.default.moveItem(at: from, to: destinationFile)
63-
65+
6466
return destinationFile
6567
}
66-
67-
static func getDirectoryForFileDestination(_ copyToDirectory: String) -> URL {
68+
69+
private static func getDirectoryForFileDestination(_ copyToDirectory: String) -> URL {
6870
let searchPath: FileManager.SearchPathDirectory = copyToDirectory == "documentDirectory" ? .documentDirectory : .cachesDirectory
6971
return FileManager.default.urls(for: searchPath, in: .userDomainMask).first!
7072
}

0 commit comments

Comments
 (0)