From df16d4e7bde3809e2091add6459b77d500351fb8 Mon Sep 17 00:00:00 2001 From: stuartmorgan Date: Wed, 22 May 2024 23:21:32 -0400 Subject: [PATCH] [file_selector] Convert iOS to Swift and SPM (#6755) Converts the plugin and its tests to Swift, and re-adds the SPM support that was reverted due to modulemap issues. In order to avoid the ugliness and loss of type saftey of using associated objects in the Swift version, this replaces that with a bridge object that serves as a delegate instead of the plugin, and manages its own lifetime in coordination with the plugin. While this is one PR, the conversion was done in individual steps, each of which is a commit: - Rewrite just the tests in Swift, with no implementation changes, ensuring that everything still passed. - Rewrite the implementation in Swift, changing the tests only as necessary for the structural changes to the implementation due to the removal of associated objects - Re-add SPM The changes in the generated Dart files are just due to updating to the latest version of Pigeon (to avoid writing the Swift implementation against an older version of the Swift API, and then having to update again later for breaking changes). Part of https://github.com/flutter/flutter/issues/119015 Fixes https://github.com/flutter/flutter/issues/146903 --- .../file_selector_ios/CHANGELOG.md | 5 + .../ios/Runner.xcodeproj/project.pbxproj | 16 +- .../xcshareddata/xcschemes/Runner.xcscheme | 18 ++ .../example/ios/Runner/AppDelegate.swift | 2 +- .../ios/RunnerTests/FileSelectorTests.m | 84 --------- .../ios/RunnerTests/FileSelectorTests.swift | 85 +++++++++ .../ios/file_selector_ios.podspec | 7 +- .../ios/file_selector_ios/Package.swift | 31 ++++ .../file_selector_ios/FFSFileSelectorPlugin.m | 107 ------------ .../FileSelectorPlugin.swift | 80 +++++++++ .../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 | 6 +- .../test/file_selector_ios_test.dart | 2 +- .../file_selector_ios/test/test_api.g.dart | 27 +-- 24 files changed, 487 insertions(+), 474 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 create mode 100644 packages/file_selector/file_selector_ios/ios/file_selector_ios/Package.swift 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/CHANGELOG.md b/packages/file_selector/file_selector_ios/CHANGELOG.md index 659188337d5f..5ee388e7a030 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/example/ios/Runner.xcodeproj/project.pbxproj b/packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.pbxproj index f6d350beac69..cca3c71f4bec 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 @@ -9,13 +9,13 @@ /* 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 828f48e73f80..5b6df8a1c381 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 000000000000..0466b808ed11 --- /dev/null +++ b/packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.swift @@ -0,0 +1,85 @@ +// 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 + +@testable import file_selector_ios + +final class TestViewPresenter: ViewPresenter { + public var presentedController: UIViewController? + + func present( + _ viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)? = nil + ) { + presentedController = viewControllerToPresent + } +} + +class FileSelectorTests: XCTestCase { + func testPickerPresents() throws { + let plugin = FileSelectorPlugin() + let picker = UIDocumentPickerViewController(documentTypes: [], in: UIDocumentPickerMode.import) + let presenter = TestViewPresenter() + plugin.documentPickerViewControllerOverride = picker + plugin.viewPresenterOverride = presenter + + plugin.openFile( + config: FileSelectorConfig(utis: [], allowMultiSelection: false) + ) { _ in } + + XCTAssertEqual(plugin.pendingCompletions.count, 1) + XCTAssertTrue(picker.delegate === plugin.pendingCompletions.first) + XCTAssertTrue(presenter.presentedController === picker) + } + + func testReturnsPickedFiles() throws { + let plugin = FileSelectorPlugin() + let picker = UIDocumentPickerViewController(documentTypes: [], in: UIDocumentPickerMode.import) + plugin.documentPickerViewControllerOverride = picker + plugin.viewPresenterOverride = TestViewPresenter() + let completionWasCalled = expectation(description: "completion") + + 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.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 = FileSelectorPlugin() + let picker = UIDocumentPickerViewController(documentTypes: [], in: UIDocumentPickerMode.import) + plugin.documentPickerViewControllerOverride = picker + plugin.viewPresenterOverride = TestViewPresenter() + let completionWasCalled = expectation(description: "completion") + + 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.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 b023e345611b..6d09c9b31055 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/Package.swift b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Package.swift new file mode 100644 index 000000000000..0b32b0813470 --- /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") + ] + ) + ] +) 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 f91820647499..000000000000 --- 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 000000000000..f1f72cc2e281 --- /dev/null +++ b/packages/file_selector/file_selector_ios/ios/file_selector_ios/Sources/file_selector_ios/FileSelectorPlugin.swift @@ -0,0 +1,80 @@ +// 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({ $0.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( + // See comment in messages.dart for why this is safe. + documentTypes: config.utis as! [String], + in: .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 000000000000..4a702bdbef4e --- /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 4ff40260ffb3..000000000000 --- 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 ca7ca56f3bd4..000000000000 --- 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 a5e2040fc634..000000000000 --- 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 d79d3642b3e8..000000000000 --- 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 d0f9d977f4b5..000000000000 --- 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 eb5b16509ee5..000000000000 --- 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 000000000000..43f1bffeac8c --- /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 3c2e4a2b8a99..d0eb22762bc9 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 6ec723c8b70f..e86dd9c90572 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 b7cd3c996bd6..a793a2af5a98 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 f91652e93fe9..24c0d84231b6 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 @@ -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 9c065f3bd1e8..41c6fe869d2b 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 7b0ace73ef9e..db24932f71a3 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.');