From a8b038b3afd864a5e8d3439efffffcf5680b6d5c Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 17 May 2024 10:07:15 -0400 Subject: [PATCH 1/5] Convert tests to Swift --- .../ios/Runner.xcodeproj/project.pbxproj | 18 ++-- .../xcshareddata/xcschemes/Runner.xcscheme | 18 ++++ .../example/ios/Runner/AppDelegate.swift | 2 +- .../ios/RunnerTests/FileSelectorTests.m | 84 ------------------- .../ios/RunnerTests/FileSelectorTests.swift | 71 ++++++++++++++++ 5 files changed, 103 insertions(+), 90 deletions(-) delete mode 100644 packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.m create mode 100644 packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.swift diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.pbxproj index f6d350beac6..bcb84219086 100644 --- a/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,19 +3,19 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 60; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 21160A929DC757957DE39F1E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 000792269CB6B9FE88AC567C /* Pods_Runner.framework */; }; + 337EF9CE2BF7945F0079FB1A /* FileSelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 337EF9CD2BF7945F0079FB1A /* FileSelectorTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 6165A2F80DFA224EAF50A1D5 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AC3841659BF3693FAC5A2F8F /* Pods_RunnerTests.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - C71AE4C8281C6B6B0086307A /* FileSelectorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C71AE4C5281C6B530086307A /* FileSelectorTests.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -45,6 +45,7 @@ 000792269CB6B9FE88AC567C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 337EF9CD2BF7945F0079FB1A /* FileSelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSelectorTests.swift; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 4A27CC0DB4EF6669B637A1E8 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; 5667547C6832727A744371E2 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; @@ -63,7 +64,6 @@ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; AC3841659BF3693FAC5A2F8F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C71AE4B6281C6A090086307A /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - C71AE4C5281C6B530086307A /* FileSelectorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FileSelectorTests.m; sourceTree = ""; }; F818CE2D7CDF8AFF94707327 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -150,7 +150,7 @@ C71AE4C4281C6B370086307A /* RunnerTests */ = { isa = PBXGroup; children = ( - C71AE4C5281C6B530086307A /* FileSelectorTests.m */, + 337EF9CD2BF7945F0079FB1A /* FileSelectorTests.swift */, ); path = RunnerTests; sourceTree = ""; @@ -223,6 +223,7 @@ }; C71AE4B5281C6A090086307A = { CreatedOnToolsVersion = 13.1; + LastSwiftMigration = 1510; TestTargetID = 97C146ED1CF9000F007C117D; }; }; @@ -376,7 +377,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - C71AE4C8281C6B6B0086307A /* FileSelectorTests.m in Sources */, + 337EF9CE2BF7945F0079FB1A /* FileSelectorTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -636,6 +637,7 @@ baseConfigurationReference = 4A27CC0DB4EF6669B637A1E8 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; @@ -647,6 +649,8 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.FileSelectorTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; name = Debug; @@ -659,6 +663,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; @@ -678,6 +683,7 @@ PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.FileSelectorTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; @@ -691,6 +697,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; @@ -710,6 +717,7 @@ PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.FileSelectorTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; }; diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 828f48e73f8..5b6df8a1c38 100644 --- a/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -5,6 +5,24 @@ + + + + + + + + + + -@property(nonatomic) UIViewController *presentedController; -@end - -@implementation TestPresenter -- (void)presentViewController:(UIViewController *)viewControllerToPresent - animated:(BOOL)animated - completion:(void (^__nullable)(void))completion { - self.presentedController = viewControllerToPresent; -} -@end - -#pragma mark - - -@interface FileSelectorTests : XCTestCase - -@end - -@implementation FileSelectorTests - -- (void)testPickerPresents { - FFSFileSelectorPlugin *plugin = [[FFSFileSelectorPlugin alloc] init]; - UIDocumentPickerViewController *picker = - [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[] - inMode:UIDocumentPickerModeImport]; - TestPresenter *presenter = [[TestPresenter alloc] init]; - plugin.documentPickerViewControllerOverride = picker; - plugin.viewPresenterOverride = presenter; - - [plugin openFileSelectorWithConfig:[FFSFileSelectorConfig makeWithUtis:@[] allowMultiSelection:NO] - completion:^(NSArray *paths, FlutterError *error){ - }]; - - XCTAssertEqualObjects(picker.delegate, plugin); - XCTAssertEqualObjects(presenter.presentedController, picker); -} - -- (void)testReturnsPickedFiles { - FFSFileSelectorPlugin *plugin = [[FFSFileSelectorPlugin alloc] init]; - XCTestExpectation *completionWasCalled = [self expectationWithDescription:@"completion"]; - UIDocumentPickerViewController *picker = - [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[] - inMode:UIDocumentPickerModeImport]; - plugin.documentPickerViewControllerOverride = picker; - [plugin openFileSelectorWithConfig:[FFSFileSelectorConfig makeWithUtis:@[] - allowMultiSelection:YES] - completion:^(NSArray *paths, FlutterError *error) { - NSArray *expectedPaths = @[ @"/file1.txt", @"/file2.txt" ]; - XCTAssertEqualObjects(paths, expectedPaths); - [completionWasCalled fulfill]; - }]; - [plugin documentPicker:picker - didPickDocumentsAtURLs:@[ - [NSURL URLWithString:@"file:///file1.txt"], [NSURL URLWithString:@"file:///file2.txt"] - ]]; - [self waitForExpectationsWithTimeout:1.0 handler:nil]; -} - -- (void)testCancellingPickerReturnsNil { - FFSFileSelectorPlugin *plugin = [[FFSFileSelectorPlugin alloc] init]; - UIDocumentPickerViewController *picker = - [[UIDocumentPickerViewController alloc] initWithDocumentTypes:@[] - inMode:UIDocumentPickerModeImport]; - plugin.documentPickerViewControllerOverride = picker; - - XCTestExpectation *completionWasCalled = [self expectationWithDescription:@"completion"]; - [plugin openFileSelectorWithConfig:[FFSFileSelectorConfig makeWithUtis:@[] allowMultiSelection:NO] - completion:^(NSArray *paths, FlutterError *error) { - XCTAssertEqual(paths.count, 0); - [completionWasCalled fulfill]; - }]; - [plugin documentPickerWasCancelled:picker]; - [self waitForExpectationsWithTimeout:1.0 handler:nil]; -} - -@end diff --git a/packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.swift b/packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.swift new file mode 100644 index 00000000000..b2d548807cc --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.swift @@ -0,0 +1,71 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import XCTest +import file_selector_ios +import file_selector_ios.Test + +class TestViewPresenter: NSObject, FFSViewPresenter { + public var presentedController: UIViewController? + + func present( + _ viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)? = nil + ) { + presentedController = viewControllerToPresent + } +} + +class FileSelectorTests: XCTestCase { + func testPickerPresents() throws { + let plugin = FFSFileSelectorPlugin() + let picker = UIDocumentPickerViewController(documentTypes: [], in: UIDocumentPickerMode.import) + let presenter = TestViewPresenter() + plugin.documentPickerViewControllerOverride = picker + plugin.viewPresenterOverride = presenter + + plugin.openFileSelector( + with: FFSFileSelectorConfig.make(withUtis: [], allowMultiSelection: false) + ) { _, _ in } + + XCTAssertTrue(picker.delegate === plugin) + XCTAssertTrue(presenter.presentedController === picker) + } + + func testReturnsPickedFiles() throws { + let plugin = FFSFileSelectorPlugin() + let picker = UIDocumentPickerViewController(documentTypes: [], in: UIDocumentPickerMode.import) + plugin.documentPickerViewControllerOverride = picker + let completionWasCalled = expectation(description: "completion") + + plugin.openFileSelector( + with: FFSFileSelectorConfig.make(withUtis: [], allowMultiSelection: false) + ) { paths, error in + let expectedPaths = ["/file1.txt", "/file2.txt"] + XCTAssertEqual(paths, expectedPaths) + completionWasCalled.fulfill() + } + plugin.documentPicker( + picker, + didPickDocumentsAt: [URL(string: "file:///file1.txt")!, URL(string: "file:///file2.txt")!]) + + waitForExpectations(timeout: 30.0) + } + + func testCancellingPickerReturnsEmptyList() throws { + let plugin = FFSFileSelectorPlugin() + let picker = UIDocumentPickerViewController(documentTypes: [], in: UIDocumentPickerMode.import) + plugin.documentPickerViewControllerOverride = picker + let completionWasCalled = expectation(description: "completion") + + plugin.openFileSelector( + with: FFSFileSelectorConfig.make(withUtis: [], allowMultiSelection: false) + ) { paths, error in + XCTAssertEqual(paths!.count, 0) + completionWasCalled.fulfill() + } + plugin.documentPickerWasCancelled(picker) + + waitForExpectations(timeout: 30.0) + } +} From 02b7305b8420d2cdecc972ee0e6c293c7f976d15 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 17 May 2024 12:21:46 -0400 Subject: [PATCH 2/5] Convert implementation to Swift --- .../ios/Runner.xcodeproj/project.pbxproj | 2 +- .../ios/RunnerTests/FileSelectorTests.swift | 56 +++--- .../ios/file_selector_ios.podspec | 7 +- .../file_selector_ios/FFSFileSelectorPlugin.m | 107 ------------ .../FileSelectorPlugin.swift | 84 +++++++++ .../file_selector_ios/ViewPresenter.swift | 20 +++ .../cocoapods_file_selector_ios.modulemap | 10 -- .../file_selector_ios/FFSFileSelectorPlugin.h | 8 - .../FFSFileSelectorPlugin_Test.h | 29 ---- .../file_selector_ios-umbrella.h | 6 - .../include/file_selector_ios/messages.g.h | 39 ----- .../Sources/file_selector_ios/messages.g.m | 136 --------------- .../file_selector_ios/messages.g.swift | 164 ++++++++++++++++++ .../lib/file_selector_ios.dart | 3 +- .../file_selector_ios/lib/src/messages.g.dart | 65 ++++--- .../file_selector_ios/pigeons/messages.dart | 11 +- .../file_selector_ios/pubspec.yaml | 4 +- .../test/file_selector_ios_test.dart | 2 +- .../file_selector_ios/test/test_api.g.dart | 27 +-- 19 files changed, 374 insertions(+), 406 deletions(-) delete mode 100644 packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/FFSFileSelectorPlugin.m create mode 100644 packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/FileSelectorPlugin.swift create mode 100644 packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/ViewPresenter.swift delete mode 100644 packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/cocoapods_file_selector_ios.modulemap delete mode 100644 packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/FFSFileSelectorPlugin.h delete mode 100644 packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/FFSFileSelectorPlugin_Test.h delete mode 100644 packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/file_selector_ios-umbrella.h delete mode 100644 packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/messages.g.h delete mode 100644 packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/messages.g.m create mode 100644 packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/messages.g.swift diff --git a/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.pbxproj b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.pbxproj index bcb84219086..cca3c71f4be 100644 --- a/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ diff --git a/packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.swift b/packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.swift index b2d548807cc..9a068b9d695 100644 --- a/packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.swift +++ b/packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.swift @@ -3,10 +3,10 @@ // found in the LICENSE file. import XCTest -import file_selector_ios -import file_selector_ios.Test -class TestViewPresenter: NSObject, FFSViewPresenter { +@testable import file_selector_ios + +class TestViewPresenter: ViewPresenter { public var presentedController: UIViewController? func present( @@ -18,54 +18,68 @@ class TestViewPresenter: NSObject, FFSViewPresenter { class FileSelectorTests: XCTestCase { func testPickerPresents() throws { - let plugin = FFSFileSelectorPlugin() + let plugin = FileSelectorPlugin() let picker = UIDocumentPickerViewController(documentTypes: [], in: UIDocumentPickerMode.import) let presenter = TestViewPresenter() plugin.documentPickerViewControllerOverride = picker plugin.viewPresenterOverride = presenter - plugin.openFileSelector( - with: FFSFileSelectorConfig.make(withUtis: [], allowMultiSelection: false) - ) { _, _ in } + plugin.openFile( + config: FileSelectorConfig(utis: [], allowMultiSelection: false) + ) { _ in } - XCTAssertTrue(picker.delegate === plugin) + XCTAssertEqual(plugin.pendingCompletions.count, 1) + XCTAssertTrue(picker.delegate === plugin.pendingCompletions.first) XCTAssertTrue(presenter.presentedController === picker) } func testReturnsPickedFiles() throws { - let plugin = FFSFileSelectorPlugin() + let plugin = FileSelectorPlugin() let picker = UIDocumentPickerViewController(documentTypes: [], in: UIDocumentPickerMode.import) plugin.documentPickerViewControllerOverride = picker + plugin.viewPresenterOverride = TestViewPresenter() let completionWasCalled = expectation(description: "completion") - plugin.openFileSelector( - with: FFSFileSelectorConfig.make(withUtis: [], allowMultiSelection: false) - ) { paths, error in - let expectedPaths = ["/file1.txt", "/file2.txt"] - XCTAssertEqual(paths, expectedPaths) + plugin.openFile( + config: FileSelectorConfig(utis: [], allowMultiSelection: false) + ) { result in + switch result { + case .success(let paths): + XCTAssertEqual(paths, ["/file1.txt", "/file2.txt"]) + case .failure(let error): + XCTFail("\(error)") + } completionWasCalled.fulfill() } - plugin.documentPicker( + plugin.pendingCompletions.first!.documentPicker( picker, didPickDocumentsAt: [URL(string: "file:///file1.txt")!, URL(string: "file:///file2.txt")!]) waitForExpectations(timeout: 30.0) + XCTAssertTrue(plugin.pendingCompletions.isEmpty) } func testCancellingPickerReturnsEmptyList() throws { - let plugin = FFSFileSelectorPlugin() + let plugin = FileSelectorPlugin() let picker = UIDocumentPickerViewController(documentTypes: [], in: UIDocumentPickerMode.import) plugin.documentPickerViewControllerOverride = picker + plugin.viewPresenterOverride = TestViewPresenter() let completionWasCalled = expectation(description: "completion") - plugin.openFileSelector( - with: FFSFileSelectorConfig.make(withUtis: [], allowMultiSelection: false) - ) { paths, error in - XCTAssertEqual(paths!.count, 0) + plugin.openFile( + config: FileSelectorConfig(utis: [], allowMultiSelection: false) + ) { result in + switch result { + case .success(let paths): + XCTAssertEqual(paths.count, 0) + case .failure(let error): + XCTFail("\(error)") + } completionWasCalled.fulfill() } - plugin.documentPickerWasCancelled(picker) + plugin.pendingCompletions.first!.documentPickerWasCancelled(picker) waitForExpectations(timeout: 30.0) + XCTAssertTrue(plugin.pendingCompletions.isEmpty) } } diff --git a/packages/file_selector/file_selector_ios/ios/file_selector_ios.podspec b/packages/file_selector/file_selector_ios/ios/file_selector_ios.podspec index b023e345611..6d09c9b3105 100644 --- a/packages/file_selector/file_selector_ios/ios/file_selector_ios.podspec +++ b/packages/file_selector/file_selector_ios/ios/file_selector_ios.podspec @@ -13,11 +13,14 @@ Displays the native iOS document picker. s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_ios' } - s.source_files = 'file_selector_ios/Sources/file_selector_ios/**/*.{h,m}' - s.module_map = 'file_selector_ios/Sources/file_selector_ios/include/cocoapods_file_selector_ios.modulemap' + s.source_files = 'file_selector_ios/Sources/file_selector_ios/**/*.swift' s.dependency 'Flutter' s.platform = :ios, '12.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' + s.xcconfig = { + 'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift', + 'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift', + } s.resource_bundles = {'file_selector_ios_privacy' => ['file_selector_ios/Sources/file_selector_ios/Resources/PrivacyInfo.xcprivacy']} end diff --git a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/FFSFileSelectorPlugin.m b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/FFSFileSelectorPlugin.m deleted file mode 100644 index f9182064749..00000000000 --- a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/FFSFileSelectorPlugin.m +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FFSFileSelectorPlugin.h" - -#import "./include/file_selector_ios/messages.g.h" -#import "FFSFileSelectorPlugin_Test.h" - -#import - -// TODO(stuartmorgan): When migrating to Swift, eliminate this in favor of -// adding FFSViewPresenter conformance to UIViewController. -@interface FFSPresentingViewController : NSObject -- (instancetype)initWithViewController:(nullable UIViewController *)controller; -// The wrapped controller. -@property(nonatomic) UIViewController *controller; -@end - -@implementation FFSPresentingViewController -- (instancetype)initWithViewController:(nullable UIViewController *)controller { - self = [super init]; - if (self) { - _controller = controller; - } - return self; -} - -- (void)presentViewController:(UIViewController *)viewControllerToPresent - animated:(BOOL)animated - completion:(void (^__nullable)(void))completion { - [self.controller presentViewController:viewControllerToPresent - animated:animated - completion:completion]; -} -@end - -#pragma mark - - -@implementation FFSFileSelectorPlugin - -#pragma mark - FFSFileSelectorApi - -- (void)openFileSelectorWithConfig:(FFSFileSelectorConfig *)config - completion:(void (^)(NSArray *_Nullable, - FlutterError *_Nullable))completion { - UIDocumentPickerViewController *documentPicker = - self.documentPickerViewControllerOverride - ?: [[UIDocumentPickerViewController alloc] - initWithDocumentTypes:config.utis - inMode:UIDocumentPickerModeImport]; - documentPicker.delegate = self; - documentPicker.allowsMultipleSelection = config.allowMultiSelection; - - id presenter = - self.viewPresenterOverride - ?: [[FFSPresentingViewController alloc] - initWithViewController:UIApplication.sharedApplication.delegate.window - .rootViewController]; - if (presenter) { - objc_setAssociatedObject(documentPicker, @selector(openFileSelectorWithConfig:completion:), - completion, OBJC_ASSOCIATION_COPY_NONATOMIC); - [presenter presentViewController:documentPicker animated:YES completion:nil]; - } else { - completion(nil, [FlutterError errorWithCode:@"error" - message:@"Missing root view controller." - details:nil]); - } -} - -#pragma mark - FlutterPlugin - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FFSFileSelectorPlugin *plugin = [[FFSFileSelectorPlugin alloc] init]; - SetUpFFSFileSelectorApi(registrar.messenger, plugin); -} - -#pragma mark - UIDocumentPickerDelegate - -- (void)documentPicker:(UIDocumentPickerViewController *)controller - didPickDocumentsAtURLs:(NSArray *)urls { - NSMutableArray *paths = [NSMutableArray arrayWithCapacity:urls.count]; - for (NSURL *url in urls) { - [paths addObject:url.path]; - }; - [self sendBackResults:paths error:nil forPicker:controller]; -} - -- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller { - [self sendBackResults:@[] error:nil forPicker:controller]; -} - -#pragma mark - Helper Methods - -- (void)sendBackResults:(NSArray *)results - error:(FlutterError *)error - forPicker:(UIDocumentPickerViewController *)picker { - void (^completionBlock)(NSArray *, FlutterError *) = - objc_getAssociatedObject(picker, @selector(openFileSelectorWithConfig:completion:)); - if (completionBlock) { - completionBlock(results, error); - objc_setAssociatedObject(picker, @selector(openFileSelectorWithConfig:completion:), nil, - OBJC_ASSOCIATION_ASSIGN); - } -} - -@end diff --git a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/FileSelectorPlugin.swift b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/FileSelectorPlugin.swift new file mode 100644 index 00000000000..1a6fe650dc5 --- /dev/null +++ b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/FileSelectorPlugin.swift @@ -0,0 +1,84 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import Flutter +import ObjectiveC +import UIKit + +/// Bridge between a UIDocumentPickerViewController and its Pigeon callback. +class PickerCompletionBridge: NSObject, UIDocumentPickerDelegate { + let completion: (Result<[String], Error>) -> Void + /// The plugin instance that owns this object, to ensure that it lives as long as the picker it + /// serves as a delegate for. Instances are responsible for removing themselves from their owner + /// on completion. + let owner: FileSelectorPlugin + + init(completion: @escaping (Result<[String], Error>) -> Void, owner: FileSelectorPlugin) { + self.completion = completion + self.owner = owner + } + + func documentPicker( + _ controller: UIDocumentPickerViewController, + didPickDocumentsAt urls: [URL] + ) { + sendResult( + urls.map({ document in + document.path + })) + } + + func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + sendResult([]) + } + + private func sendResult(_ result: [String]) { + completion(.success(result)) + owner.pendingCompletions.remove(self) + } +} + +public class FileSelectorPlugin: NSObject, FlutterPlugin, FileSelectorApi { + /// Owning references to pending completion callbacks. + /// + /// This is necessary since the objects need to live until a UIDocumentPickerDelegate method is + /// called on the delegate, but the delegate is weak. Objects in this set are responsible for + /// removing themselves from it. + var pendingCompletions: Set = [] + /// Overridden document picker, for testing. + var documentPickerViewControllerOverride: UIDocumentPickerViewController? + /// Overridden view presenter, for testing. + var viewPresenterOverride: ViewPresenter? + + public static func register(with registrar: FlutterPluginRegistrar) { + let instance = FileSelectorPlugin() + FileSelectorApiSetup.setUp(binaryMessenger: registrar.messenger(), api: instance) + } + + func openFile(config: FileSelectorConfig, completion: @escaping (Result<[String], Error>) -> Void) + { + let completionBridge = PickerCompletionBridge(completion: completion, owner: self) + let documentPicker = + documentPickerViewControllerOverride + ?? UIDocumentPickerViewController( + documentTypes: config.utis.map({ uti in + // See comment in messages.dart for why this is safe. + uti! + }), in: UIDocumentPickerMode.import) + documentPicker.allowsMultipleSelection = config.allowMultiSelection + documentPicker.delegate = completionBridge + + let presenter = + self.viewPresenterOverride ?? UIApplication.shared.delegate?.window??.rootViewController + if let presenter = presenter { + pendingCompletions.insert(completionBridge) + presenter.present(documentPicker, animated: true, completion: nil) + } else { + completion( + .failure(PigeonError(code: "error", message: "Missing root view controller.", details: nil)) + ) + } + } + +} diff --git a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/ViewPresenter.swift b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/ViewPresenter.swift new file mode 100644 index 00000000000..4a702bdbef4 --- /dev/null +++ b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/ViewPresenter.swift @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import UIKit + +/// Protocol for UIViewController methods relating to presenting a controller. +/// +/// This protocol exists to allow injecting an alternate implementation for testing. +protocol ViewPresenter { + /// Presents a view controller modally. + func present( + _ viewControllerToPresent: UIViewController, + animated flag: Bool, + completion: (() -> Void)? + ) +} + +/// ViewPresenter is intentionally a direct passthroguh to UIViewController. +extension UIViewController: ViewPresenter {} diff --git a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/cocoapods_file_selector_ios.modulemap b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/cocoapods_file_selector_ios.modulemap deleted file mode 100644 index 4ff40260ffb..00000000000 --- a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/cocoapods_file_selector_ios.modulemap +++ /dev/null @@ -1,10 +0,0 @@ -framework module file_selector_ios { - umbrella header "file_selector_ios-umbrella.h" - - export * - module * { export * } - - explicit module Test { - header "FFSFileSelectorPlugin_Test.h" - } -} diff --git a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/FFSFileSelectorPlugin.h b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/FFSFileSelectorPlugin.h deleted file mode 100644 index ca7ca56f3bd..00000000000 --- a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/FFSFileSelectorPlugin.h +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -@interface FFSFileSelectorPlugin : NSObject -@end diff --git a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/FFSFileSelectorPlugin_Test.h b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/FFSFileSelectorPlugin_Test.h deleted file mode 100644 index a5e2040fc63..00000000000 --- a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/FFSFileSelectorPlugin_Test.h +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FFSFileSelectorPlugin.h" - -@import UIKit; - -#import "messages.g.h" - -/// Interface for presenting a view controller, to allow injecting an alternate -/// test implementation. -@protocol FFSViewPresenter -/// Wrapper for -[UIViewController presentViewController:animated:completion:]. -- (void)presentViewController:(UIViewController *_Nonnull)viewControllerToPresent - animated:(BOOL)animated - completion:(void (^__nullable)(void))completion; -@end - -// This header is available in the Test module. Import via "@import file_selector_ios.Test;". -@interface FFSFileSelectorPlugin () - -/// Overrides the view controller used for presenting the document picker. -@property(nonatomic) id _Nullable viewPresenterOverride; - -/// Overrides the UIDocumentPickerViewController used for file picking. -@property(nonatomic) UIDocumentPickerViewController *_Nullable documentPickerViewControllerOverride; - -@end diff --git a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/file_selector_ios-umbrella.h b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/file_selector_ios-umbrella.h deleted file mode 100644 index d79d3642b3e..00000000000 --- a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/file_selector_ios-umbrella.h +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import diff --git a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/messages.g.h b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/messages.g.h deleted file mode 100644 index d0f9d977f4b..00000000000 --- a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/messages.g.h +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -// Autogenerated from Pigeon (v13.0.0), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -#import - -@protocol FlutterBinaryMessenger; -@protocol FlutterMessageCodec; -@class FlutterError; -@class FlutterStandardTypedData; - -NS_ASSUME_NONNULL_BEGIN - -@class FFSFileSelectorConfig; - -@interface FFSFileSelectorConfig : NSObject -/// `init` unavailable to enforce nonnull fields, see the `make` class method. -- (instancetype)init NS_UNAVAILABLE; -+ (instancetype)makeWithUtis:(NSArray *)utis - allowMultiSelection:(BOOL)allowMultiSelection; -@property(nonatomic, copy) NSArray *utis; -@property(nonatomic, assign) BOOL allowMultiSelection; -@end - -/// The codec used by FFSFileSelectorApi. -NSObject *FFSFileSelectorApiGetCodec(void); - -@protocol FFSFileSelectorApi -- (void)openFileSelectorWithConfig:(FFSFileSelectorConfig *)config - completion:(void (^)(NSArray *_Nullable, - FlutterError *_Nullable))completion; -@end - -extern void SetUpFFSFileSelectorApi(id binaryMessenger, - NSObject *_Nullable api); - -NS_ASSUME_NONNULL_END diff --git a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/messages.g.m b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/messages.g.m deleted file mode 100644 index eb5b16509ee..00000000000 --- a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/messages.g.m +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2013 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -// Autogenerated from Pigeon (v13.0.0), do not edit directly. -// See also: https://pub.dev/packages/pigeon - -#import "./include/file_selector_ios/messages.g.h" - -#if TARGET_OS_OSX -#import -#else -#import -#endif - -#if !__has_feature(objc_arc) -#error File requires ARC to be enabled. -#endif - -static NSArray *wrapResult(id result, FlutterError *error) { - if (error) { - return @[ - error.code ?: [NSNull null], error.message ?: [NSNull null], error.details ?: [NSNull null] - ]; - } - return @[ result ?: [NSNull null] ]; -} -static id GetNullableObjectAtIndex(NSArray *array, NSInteger key) { - id result = array[key]; - return (result == [NSNull null]) ? nil : result; -} - -@interface FFSFileSelectorConfig () -+ (FFSFileSelectorConfig *)fromList:(NSArray *)list; -+ (nullable FFSFileSelectorConfig *)nullableFromList:(NSArray *)list; -- (NSArray *)toList; -@end - -@implementation FFSFileSelectorConfig -+ (instancetype)makeWithUtis:(NSArray *)utis - allowMultiSelection:(BOOL)allowMultiSelection { - FFSFileSelectorConfig *pigeonResult = [[FFSFileSelectorConfig alloc] init]; - pigeonResult.utis = utis; - pigeonResult.allowMultiSelection = allowMultiSelection; - return pigeonResult; -} -+ (FFSFileSelectorConfig *)fromList:(NSArray *)list { - FFSFileSelectorConfig *pigeonResult = [[FFSFileSelectorConfig alloc] init]; - pigeonResult.utis = GetNullableObjectAtIndex(list, 0); - pigeonResult.allowMultiSelection = [GetNullableObjectAtIndex(list, 1) boolValue]; - return pigeonResult; -} -+ (nullable FFSFileSelectorConfig *)nullableFromList:(NSArray *)list { - return (list) ? [FFSFileSelectorConfig fromList:list] : nil; -} -- (NSArray *)toList { - return @[ - self.utis ?: [NSNull null], - @(self.allowMultiSelection), - ]; -} -@end - -@interface FFSFileSelectorApiCodecReader : FlutterStandardReader -@end -@implementation FFSFileSelectorApiCodecReader -- (nullable id)readValueOfType:(UInt8)type { - switch (type) { - case 128: - return [FFSFileSelectorConfig fromList:[self readValue]]; - default: - return [super readValueOfType:type]; - } -} -@end - -@interface FFSFileSelectorApiCodecWriter : FlutterStandardWriter -@end -@implementation FFSFileSelectorApiCodecWriter -- (void)writeValue:(id)value { - if ([value isKindOfClass:[FFSFileSelectorConfig class]]) { - [self writeByte:128]; - [self writeValue:[value toList]]; - } else { - [super writeValue:value]; - } -} -@end - -@interface FFSFileSelectorApiCodecReaderWriter : FlutterStandardReaderWriter -@end -@implementation FFSFileSelectorApiCodecReaderWriter -- (FlutterStandardWriter *)writerWithData:(NSMutableData *)data { - return [[FFSFileSelectorApiCodecWriter alloc] initWithData:data]; -} -- (FlutterStandardReader *)readerWithData:(NSData *)data { - return [[FFSFileSelectorApiCodecReader alloc] initWithData:data]; -} -@end - -NSObject *FFSFileSelectorApiGetCodec(void) { - static FlutterStandardMessageCodec *sSharedObject = nil; - static dispatch_once_t sPred = 0; - dispatch_once(&sPred, ^{ - FFSFileSelectorApiCodecReaderWriter *readerWriter = - [[FFSFileSelectorApiCodecReaderWriter alloc] init]; - sSharedObject = [FlutterStandardMessageCodec codecWithReaderWriter:readerWriter]; - }); - return sSharedObject; -} - -void SetUpFFSFileSelectorApi(id binaryMessenger, - NSObject *api) { - { - FlutterBasicMessageChannel *channel = [[FlutterBasicMessageChannel alloc] - initWithName:@"dev.flutter.pigeon.file_selector_ios.FileSelectorApi.openFile" - binaryMessenger:binaryMessenger - codec:FFSFileSelectorApiGetCodec()]; - if (api) { - NSCAssert([api respondsToSelector:@selector(openFileSelectorWithConfig:completion:)], - @"FFSFileSelectorApi api (%@) doesn't respond to " - @"@selector(openFileSelectorWithConfig:completion:)", - api); - [channel setMessageHandler:^(id _Nullable message, FlutterReply callback) { - NSArray *args = message; - FFSFileSelectorConfig *arg_config = GetNullableObjectAtIndex(args, 0); - [api openFileSelectorWithConfig:arg_config - completion:^(NSArray *_Nullable output, - FlutterError *_Nullable error) { - callback(wrapResult(output, error)); - }]; - }]; - } else { - [channel setMessageHandler:nil]; - } - } -} diff --git a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/messages.g.swift b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/messages.g.swift new file mode 100644 index 00000000000..43f1bffeac8 --- /dev/null +++ b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/messages.g.swift @@ -0,0 +1,164 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Autogenerated from Pigeon (v19.0.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon + +import Foundation + +#if os(iOS) + import Flutter +#elseif os(macOS) + import FlutterMacOS +#else + #error("Unsupported platform.") +#endif + +/// Error class for passing custom error details to Dart side. +final class PigeonError: Error { + let code: String + let message: String? + let details: Any? + + init(code: String, message: String?, details: Any?) { + self.code = code + self.message = message + self.details = details + } + + var localizedDescription: String { + return + "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")" + } +} + +private func wrapResult(_ result: Any?) -> [Any?] { + return [result] +} + +private func wrapError(_ error: Any) -> [Any?] { + if let pigeonError = error as? PigeonError { + return [ + pigeonError.code, + pigeonError.message, + pigeonError.details, + ] + } + if let flutterError = error as? FlutterError { + return [ + flutterError.code, + flutterError.message, + flutterError.details, + ] + } + return [ + "\(error)", + "\(type(of: error))", + "Stacktrace: \(Thread.callStackSymbols)", + ] +} + +private func isNullish(_ value: Any?) -> Bool { + return value is NSNull || value == nil +} + +private func nilOrValue(_ value: Any?) -> T? { + if value is NSNull { return nil } + return value as! T? +} + +/// Generated class from Pigeon that represents data sent in messages. +struct FileSelectorConfig { + var utis: [String?] + var allowMultiSelection: Bool + + // swift-format-ignore: AlwaysUseLowerCamelCase + static func fromList(_ __pigeon_list: [Any?]) -> FileSelectorConfig? { + let utis = __pigeon_list[0] as! [String?] + let allowMultiSelection = __pigeon_list[1] as! Bool + + return FileSelectorConfig( + utis: utis, + allowMultiSelection: allowMultiSelection + ) + } + func toList() -> [Any?] { + return [ + utis, + allowMultiSelection, + ] + } +} + +private class FileSelectorApiCodecReader: FlutterStandardReader { + override func readValue(ofType type: UInt8) -> Any? { + switch type { + case 128: + return FileSelectorConfig.fromList(self.readValue() as! [Any?]) + default: + return super.readValue(ofType: type) + } + } +} + +private class FileSelectorApiCodecWriter: FlutterStandardWriter { + override func writeValue(_ value: Any) { + if let value = value as? FileSelectorConfig { + super.writeByte(128) + super.writeValue(value.toList()) + } else { + super.writeValue(value) + } + } +} + +private class FileSelectorApiCodecReaderWriter: FlutterStandardReaderWriter { + override func reader(with data: Data) -> FlutterStandardReader { + return FileSelectorApiCodecReader(data: data) + } + + override func writer(with data: NSMutableData) -> FlutterStandardWriter { + return FileSelectorApiCodecWriter(data: data) + } +} + +class FileSelectorApiCodec: FlutterStandardMessageCodec { + static let shared = FileSelectorApiCodec(readerWriter: FileSelectorApiCodecReaderWriter()) +} + +/// Generated protocol from Pigeon that represents a handler of messages from Flutter. +protocol FileSelectorApi { + func openFile(config: FileSelectorConfig, completion: @escaping (Result<[String], Error>) -> Void) +} + +/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`. +class FileSelectorApiSetup { + /// The codec used by FileSelectorApi. + static var codec: FlutterStandardMessageCodec { FileSelectorApiCodec.shared } + /// Sets up an instance of `FileSelectorApi` to handle messages through the `binaryMessenger`. + static func setUp( + binaryMessenger: FlutterBinaryMessenger, api: FileSelectorApi?, + messageChannelSuffix: String = "" + ) { + let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : "" + let openFileChannel = FlutterBasicMessageChannel( + name: "dev.flutter.pigeon.file_selector_ios.FileSelectorApi.openFile\(channelSuffix)", + binaryMessenger: binaryMessenger, codec: codec) + if let api = api { + openFileChannel.setMessageHandler { message, reply in + let args = message as! [Any?] + let configArg = args[0] as! FileSelectorConfig + api.openFile(config: configArg) { result in + switch result { + case .success(let res): + reply(wrapResult(res)) + case .failure(let error): + reply(wrapError(error)) + } + } + } + } else { + openFileChannel.setMessageHandler(nil) + } + } +} diff --git a/packages/file_selector/file_selector_ios/lib/file_selector_ios.dart b/packages/file_selector/file_selector_ios/lib/file_selector_ios.dart index 3c2e4a2b8a9..d0eb22762bc 100644 --- a/packages/file_selector/file_selector_ios/lib/file_selector_ios.dart +++ b/packages/file_selector/file_selector_ios/lib/file_selector_ios.dart @@ -22,8 +22,7 @@ class FileSelectorIOS extends FileSelectorPlatform { String? confirmButtonText, }) async { final List path = (await _hostApi.openFile(FileSelectorConfig( - utis: _allowedUtiListFromTypeGroups(acceptedTypeGroups), - allowMultiSelection: false))) + utis: _allowedUtiListFromTypeGroups(acceptedTypeGroups)))) .cast(); return path.isEmpty ? null : XFile(path.first); } diff --git a/packages/file_selector/file_selector_ios/lib/src/messages.g.dart b/packages/file_selector/file_selector_ios/lib/src/messages.g.dart index 6ec723c8b70..e86dd9c9057 100644 --- a/packages/file_selector/file_selector_ios/lib/src/messages.g.dart +++ b/packages/file_selector/file_selector_ios/lib/src/messages.g.dart @@ -1,9 +1,9 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v13.0.0), do not edit directly. +// Autogenerated from Pigeon (v19.0.0), do not edit directly. // See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers import 'dart:async'; import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; @@ -11,6 +11,13 @@ import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; import 'package:flutter/services.dart'; +PlatformException _createConnectionError(String channelName) { + return PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel: "$channelName".', + ); +} + List wrapResponse( {Object? result, PlatformException? error, bool empty = false}) { if (empty) { @@ -24,8 +31,8 @@ List wrapResponse( class FileSelectorConfig { FileSelectorConfig({ - required this.utis, - required this.allowMultiSelection, + this.utis = const [], + this.allowMultiSelection = false, }); List utis; @@ -75,36 +82,44 @@ class FileSelectorApi { /// Constructor for [FileSelectorApi]. The [binaryMessenger] named argument is /// available for dependency injection. If it is left null, the default /// BinaryMessenger will be used which routes to the host platform. - FileSelectorApi({BinaryMessenger? binaryMessenger}) - : _binaryMessenger = binaryMessenger; - final BinaryMessenger? _binaryMessenger; + FileSelectorApi( + {BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : __pigeon_binaryMessenger = binaryMessenger, + __pigeon_messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? __pigeon_binaryMessenger; - static const MessageCodec codec = _FileSelectorApiCodec(); + static const MessageCodec pigeonChannelCodec = + _FileSelectorApiCodec(); - Future> openFile(FileSelectorConfig arg_config) async { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.file_selector_ios.FileSelectorApi.openFile', codec, - binaryMessenger: _binaryMessenger); - final List? replyList = - await channel.send([arg_config]) as List?; - if (replyList == null) { - throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - ); - } else if (replyList.length > 1) { + final String __pigeon_messageChannelSuffix; + + Future> openFile(FileSelectorConfig config) async { + final String __pigeon_channelName = + 'dev.flutter.pigeon.file_selector_ios.FileSelectorApi.openFile$__pigeon_messageChannelSuffix'; + final BasicMessageChannel __pigeon_channel = + BasicMessageChannel( + __pigeon_channelName, + pigeonChannelCodec, + binaryMessenger: __pigeon_binaryMessenger, + ); + final List? __pigeon_replyList = + await __pigeon_channel.send([config]) as List?; + if (__pigeon_replyList == null) { + throw _createConnectionError(__pigeon_channelName); + } else if (__pigeon_replyList.length > 1) { throw PlatformException( - code: replyList[0]! as String, - message: replyList[1] as String?, - details: replyList[2], + code: __pigeon_replyList[0]! as String, + message: __pigeon_replyList[1] as String?, + details: __pigeon_replyList[2], ); - } else if (replyList[0] == null) { + } else if (__pigeon_replyList[0] == null) { throw PlatformException( code: 'null-error', message: 'Host platform returned null value for non-null return value.', ); } else { - return (replyList[0] as List?)!.cast(); + return (__pigeon_replyList[0] as List?)!.cast(); } } } diff --git a/packages/file_selector/file_selector_ios/pigeons/messages.dart b/packages/file_selector/file_selector_ios/pigeons/messages.dart index b7cd3c996bd..a793a2af5a9 100644 --- a/packages/file_selector/file_selector_ios/pigeons/messages.dart +++ b/packages/file_selector/file_selector_ios/pigeons/messages.dart @@ -7,18 +7,15 @@ import 'package:pigeon/pigeon.dart'; @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/messages.g.dart', dartTestOut: 'test/test_api.g.dart', - objcHeaderOut: - 'ios/file_selector_ios/Sources/file_selector_ios/include/file_selector_ios/messages.g.h', - objcSourceOut: 'ios/file_selector_ios/Sources/file_selector_ios/messages.g.m', - objcOptions: ObjcOptions( - prefix: 'FFS', - headerIncludePath: './include/file_selector_ios/messages.g.h', - ), + swiftOut: 'ios/file_selector_ios/Sources/file_selector_ios/messages.g.swift', copyrightHeader: 'pigeons/copyright.txt', )) class FileSelectorConfig { FileSelectorConfig( {this.utis = const [], this.allowMultiSelection = false}); + // TODO(stuartmorgan): Declare these as non-nullable generics once + // https://github.com/flutter/flutter/issues/97848 is fixed. In practice, + // the values will never be null, and the native implementation assumes that. List utis; bool allowMultiSelection; } diff --git a/packages/file_selector/file_selector_ios/pubspec.yaml b/packages/file_selector/file_selector_ios/pubspec.yaml index f91652e93fe..7e9fd8198c4 100644 --- a/packages/file_selector/file_selector_ios/pubspec.yaml +++ b/packages/file_selector/file_selector_ios/pubspec.yaml @@ -14,7 +14,7 @@ flutter: platforms: ios: dartPluginClass: FileSelectorIOS - pluginClass: FFSFileSelectorPlugin + pluginClass: FileSelectorPlugin dependencies: file_selector_platform_interface: ^2.3.0 @@ -26,7 +26,7 @@ dev_dependencies: flutter_test: sdk: flutter mockito: 5.4.4 - pigeon: ^13.0.0 + pigeon: ^19.0.0 topics: - files diff --git a/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart b/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart index 9c065f3bd1e..41c6fe869d2 100644 --- a/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart +++ b/packages/file_selector/file_selector_ios/test/file_selector_ios_test.dart @@ -22,7 +22,7 @@ void main() { setUp(() { mockApi = MockTestFileSelectorApi(); - TestFileSelectorApi.setup(mockApi); + TestFileSelectorApi.setUp(mockApi); }); test('registered instance', () { diff --git a/packages/file_selector/file_selector_ios/test/test_api.g.dart b/packages/file_selector/file_selector_ios/test/test_api.g.dart index 7b0ace73ef9..db24932f71a 100644 --- a/packages/file_selector/file_selector_ios/test/test_api.g.dart +++ b/packages/file_selector/file_selector_ios/test/test_api.g.dart @@ -1,9 +1,9 @@ // Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Autogenerated from Pigeon (v13.0.0), do not edit directly. +// Autogenerated from Pigeon (v19.0.0), do not edit directly. // See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, unnecessary_import, no_leading_underscores_for_local_identifiers // ignore_for_file: avoid_relative_lib_imports import 'dart:async'; import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; @@ -39,23 +39,30 @@ class _TestFileSelectorApiCodec extends StandardMessageCodec { abstract class TestFileSelectorApi { static TestDefaultBinaryMessengerBinding? get _testBinaryMessengerBinding => TestDefaultBinaryMessengerBinding.instance; - static const MessageCodec codec = _TestFileSelectorApiCodec(); + static const MessageCodec pigeonChannelCodec = + _TestFileSelectorApiCodec(); Future> openFile(FileSelectorConfig config); - static void setup(TestFileSelectorApi? api, - {BinaryMessenger? binaryMessenger}) { + static void setUp( + TestFileSelectorApi? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; { - final BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.file_selector_ios.FileSelectorApi.openFile', - codec, + final BasicMessageChannel __pigeon_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.file_selector_ios.FileSelectorApi.openFile$messageChannelSuffix', + pigeonChannelCodec, binaryMessenger: binaryMessenger); if (api == null) { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, null); + .setMockDecodedMessageHandler(__pigeon_channel, null); } else { _testBinaryMessengerBinding!.defaultBinaryMessenger - .setMockDecodedMessageHandler(channel, + .setMockDecodedMessageHandler(__pigeon_channel, (Object? message) async { assert(message != null, 'Argument for dev.flutter.pigeon.file_selector_ios.FileSelectorApi.openFile was null.'); From 2b49872e9aa75c3512bd34c386082b2205f5b6ed Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 17 May 2024 12:25:52 -0400 Subject: [PATCH 3/5] Re-add SPM support --- .../ios/file_selector_ios/Package.swift | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 packages/file_selector/file_selector_ios/ios/file_selector_ios/Package.swift diff --git a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Package.swift b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Package.swift new file mode 100644 index 00000000000..0b32b081347 --- /dev/null +++ b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version: 5.9 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import PackageDescription + +let package = Package( + name: "file_selector_ios", + platforms: [ + .iOS("12.0") + ], + products: [ + .library(name: "file-selector-ios", targets: ["file_selector_ios"]) + ], + dependencies: [], + targets: [ + .target( + name: "file_selector_ios", + dependencies: [], + resources: [ + .process("Resources") + ], + cSettings: [ + .headerSearchPath("include/file_selector_ios") + ] + ) + ] +) From 6ce49a397a290c93af45006278d13fd13b495584 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Fri, 17 May 2024 12:25:59 -0400 Subject: [PATCH 4/5] Version bump --- packages/file_selector/file_selector_ios/CHANGELOG.md | 5 +++++ packages/file_selector/file_selector_ios/pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/file_selector/file_selector_ios/CHANGELOG.md b/packages/file_selector/file_selector_ios/CHANGELOG.md index 659188337d5..5ee388e7a03 100644 --- a/packages/file_selector/file_selector_ios/CHANGELOG.md +++ b/packages/file_selector/file_selector_ios/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.5.3 + +* Converts implementation to Swift. +* Re-adds Swift Package Manager compatibility. + ## 0.5.2+1 * Temporarily remove Swift Package Manager compatibility to resolve issues with Cocoapods builds. diff --git a/packages/file_selector/file_selector_ios/pubspec.yaml b/packages/file_selector/file_selector_ios/pubspec.yaml index 7e9fd8198c4..24c0d84231b 100644 --- a/packages/file_selector/file_selector_ios/pubspec.yaml +++ b/packages/file_selector/file_selector_ios/pubspec.yaml @@ -2,7 +2,7 @@ name: file_selector_ios description: iOS implementation of the file_selector plugin. repository: https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_ios issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 -version: 0.5.2+1 +version: 0.5.3 environment: sdk: ^3.2.3 From 3dd7d9d6afab8f1b0a1cb27fc162470ada5eff79 Mon Sep 17 00:00:00 2001 From: Stuart Morgan Date: Wed, 22 May 2024 20:32:06 -0400 Subject: [PATCH 5/5] Review feedback --- .../example/ios/RunnerTests/FileSelectorTests.swift | 2 +- .../file_selector_ios/FileSelectorPlugin.swift | 12 ++++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.swift b/packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.swift index 9a068b9d695..0466b808ed1 100644 --- a/packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.swift +++ b/packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.swift @@ -6,7 +6,7 @@ import XCTest @testable import file_selector_ios -class TestViewPresenter: ViewPresenter { +final class TestViewPresenter: ViewPresenter { public var presentedController: UIViewController? func present( diff --git a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/FileSelectorPlugin.swift b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/FileSelectorPlugin.swift index 1a6fe650dc5..f1f72cc2e28 100644 --- a/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/FileSelectorPlugin.swift +++ b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/FileSelectorPlugin.swift @@ -23,10 +23,7 @@ class PickerCompletionBridge: NSObject, UIDocumentPickerDelegate { _ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL] ) { - sendResult( - urls.map({ document in - document.path - })) + sendResult(urls.map({ $0.path })) } func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { @@ -62,10 +59,9 @@ public class FileSelectorPlugin: NSObject, FlutterPlugin, FileSelectorApi { let documentPicker = documentPickerViewControllerOverride ?? UIDocumentPickerViewController( - documentTypes: config.utis.map({ uti in - // See comment in messages.dart for why this is safe. - uti! - }), in: UIDocumentPickerMode.import) + // See comment in messages.dart for why this is safe. + documentTypes: config.utis as! [String], + in: .import) documentPicker.allowsMultipleSelection = config.allowMultiSelection documentPicker.delegate = completionBridge