Skip to content

Commit df16d4e

Browse files
[file_selector] Convert iOS to Swift and SPM (flutter#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 flutter#119015 Fixes flutter#146903
1 parent 66530f2 commit df16d4e

24 files changed

+487
-474
lines changed

packages/file_selector/file_selector_ios/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 0.5.3
2+
3+
* Converts implementation to Swift.
4+
* Re-adds Swift Package Manager compatibility.
5+
16
## 0.5.2+1
27

38
* Temporarily remove Swift Package Manager compatibility to resolve issues with Cocoapods builds.

packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
/* Begin PBXBuildFile section */
1010
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
1111
21160A929DC757957DE39F1E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 000792269CB6B9FE88AC567C /* Pods_Runner.framework */; };
12+
337EF9CE2BF7945F0079FB1A /* FileSelectorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 337EF9CD2BF7945F0079FB1A /* FileSelectorTests.swift */; };
1213
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
1314
6165A2F80DFA224EAF50A1D5 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AC3841659BF3693FAC5A2F8F /* Pods_RunnerTests.framework */; };
1415
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
1516
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
1617
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
1718
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
18-
C71AE4C8281C6B6B0086307A /* FileSelectorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = C71AE4C5281C6B530086307A /* FileSelectorTests.m */; };
1919
/* End PBXBuildFile section */
2020

2121
/* Begin PBXContainerItemProxy section */
@@ -45,6 +45,7 @@
4545
000792269CB6B9FE88AC567C /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4646
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
4747
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
48+
337EF9CD2BF7945F0079FB1A /* FileSelectorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSelectorTests.swift; sourceTree = "<group>"; };
4849
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4950
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 = "<group>"; };
5051
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 = "<group>"; };
@@ -63,7 +64,6 @@
6364
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
6465
AC3841659BF3693FAC5A2F8F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
6566
C71AE4B6281C6A090086307A /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
66-
C71AE4C5281C6B530086307A /* FileSelectorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FileSelectorTests.m; sourceTree = "<group>"; };
6767
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 = "<group>"; };
6868
/* End PBXFileReference section */
6969

@@ -150,7 +150,7 @@
150150
C71AE4C4281C6B370086307A /* RunnerTests */ = {
151151
isa = PBXGroup;
152152
children = (
153-
C71AE4C5281C6B530086307A /* FileSelectorTests.m */,
153+
337EF9CD2BF7945F0079FB1A /* FileSelectorTests.swift */,
154154
);
155155
path = RunnerTests;
156156
sourceTree = "<group>";
@@ -223,6 +223,7 @@
223223
};
224224
C71AE4B5281C6A090086307A = {
225225
CreatedOnToolsVersion = 13.1;
226+
LastSwiftMigration = 1510;
226227
TestTargetID = 97C146ED1CF9000F007C117D;
227228
};
228229
};
@@ -376,7 +377,7 @@
376377
isa = PBXSourcesBuildPhase;
377378
buildActionMask = 2147483647;
378379
files = (
379-
C71AE4C8281C6B6B0086307A /* FileSelectorTests.m in Sources */,
380+
337EF9CE2BF7945F0079FB1A /* FileSelectorTests.swift in Sources */,
380381
);
381382
runOnlyForDeploymentPostprocessing = 0;
382383
};
@@ -636,6 +637,7 @@
636637
baseConfigurationReference = 4A27CC0DB4EF6669B637A1E8 /* Pods-RunnerTests.debug.xcconfig */;
637638
buildSettings = {
638639
BUNDLE_LOADER = "$(TEST_HOST)";
640+
CLANG_ENABLE_MODULES = YES;
639641
CODE_SIGN_STYLE = Automatic;
640642
CURRENT_PROJECT_VERSION = 1;
641643
GENERATE_INFOPLIST_FILE = YES;
@@ -647,6 +649,8 @@
647649
MARKETING_VERSION = 1.0;
648650
PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.FileSelectorTests;
649651
PRODUCT_NAME = "$(TARGET_NAME)";
652+
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
653+
SWIFT_VERSION = 5.0;
650654
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
651655
};
652656
name = Debug;
@@ -659,6 +663,7 @@
659663
BUNDLE_LOADER = "$(TEST_HOST)";
660664
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
661665
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
666+
CLANG_ENABLE_MODULES = YES;
662667
CLANG_ENABLE_OBJC_WEAK = YES;
663668
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
664669
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
@@ -678,6 +683,7 @@
678683
PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.FileSelectorTests;
679684
PRODUCT_NAME = "$(TARGET_NAME)";
680685
SWIFT_EMIT_LOC_STRINGS = NO;
686+
SWIFT_VERSION = 5.0;
681687
TARGETED_DEVICE_FAMILY = "1,2";
682688
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
683689
};
@@ -691,6 +697,7 @@
691697
BUNDLE_LOADER = "$(TEST_HOST)";
692698
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
693699
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
700+
CLANG_ENABLE_MODULES = YES;
694701
CLANG_ENABLE_OBJC_WEAK = YES;
695702
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
696703
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
@@ -710,6 +717,7 @@
710717
PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.FileSelectorTests;
711718
PRODUCT_NAME = "$(TARGET_NAME)";
712719
SWIFT_EMIT_LOC_STRINGS = NO;
720+
SWIFT_VERSION = 5.0;
713721
TARGETED_DEVICE_FAMILY = "1,2";
714722
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner";
715723
};

packages/file_selector/file_selector_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@
55
<BuildAction
66
parallelizeBuildables = "YES"
77
buildImplicitDependencies = "YES">
8+
<PreActions>
9+
<ExecutionAction
10+
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
11+
<ActionContent
12+
title = "Run Prepare Flutter Framework Script"
13+
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
14+
<EnvironmentBuildable>
15+
<BuildableReference
16+
BuildableIdentifier = "primary"
17+
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
18+
BuildableName = "Runner.app"
19+
BlueprintName = "Runner"
20+
ReferencedContainer = "container:Runner.xcodeproj">
21+
</BuildableReference>
22+
</EnvironmentBuildable>
23+
</ActionContent>
24+
</ExecutionAction>
25+
</PreActions>
826
<BuildActionEntries>
927
<BuildActionEntry
1028
buildForTesting = "YES"

packages/file_selector/file_selector_ios/example/ios/Runner/AppDelegate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import Flutter
66
import UIKit
77

8-
@UIApplicationMain
8+
@main
99
@objc class AppDelegate: FlutterAppDelegate {
1010
override func application(
1111
_ application: UIApplication,

packages/file_selector/file_selector_ios/example/ios/RunnerTests/FileSelectorTests.m

Lines changed: 0 additions & 84 deletions
This file was deleted.
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import XCTest
6+
7+
@testable import file_selector_ios
8+
9+
final class TestViewPresenter: ViewPresenter {
10+
public var presentedController: UIViewController?
11+
12+
func present(
13+
_ viewControllerToPresent: UIViewController, animated: Bool, completion: (() -> Void)? = nil
14+
) {
15+
presentedController = viewControllerToPresent
16+
}
17+
}
18+
19+
class FileSelectorTests: XCTestCase {
20+
func testPickerPresents() throws {
21+
let plugin = FileSelectorPlugin()
22+
let picker = UIDocumentPickerViewController(documentTypes: [], in: UIDocumentPickerMode.import)
23+
let presenter = TestViewPresenter()
24+
plugin.documentPickerViewControllerOverride = picker
25+
plugin.viewPresenterOverride = presenter
26+
27+
plugin.openFile(
28+
config: FileSelectorConfig(utis: [], allowMultiSelection: false)
29+
) { _ in }
30+
31+
XCTAssertEqual(plugin.pendingCompletions.count, 1)
32+
XCTAssertTrue(picker.delegate === plugin.pendingCompletions.first)
33+
XCTAssertTrue(presenter.presentedController === picker)
34+
}
35+
36+
func testReturnsPickedFiles() throws {
37+
let plugin = FileSelectorPlugin()
38+
let picker = UIDocumentPickerViewController(documentTypes: [], in: UIDocumentPickerMode.import)
39+
plugin.documentPickerViewControllerOverride = picker
40+
plugin.viewPresenterOverride = TestViewPresenter()
41+
let completionWasCalled = expectation(description: "completion")
42+
43+
plugin.openFile(
44+
config: FileSelectorConfig(utis: [], allowMultiSelection: false)
45+
) { result in
46+
switch result {
47+
case .success(let paths):
48+
XCTAssertEqual(paths, ["/file1.txt", "/file2.txt"])
49+
case .failure(let error):
50+
XCTFail("\(error)")
51+
}
52+
completionWasCalled.fulfill()
53+
}
54+
plugin.pendingCompletions.first!.documentPicker(
55+
picker,
56+
didPickDocumentsAt: [URL(string: "file:///file1.txt")!, URL(string: "file:///file2.txt")!])
57+
58+
waitForExpectations(timeout: 30.0)
59+
XCTAssertTrue(plugin.pendingCompletions.isEmpty)
60+
}
61+
62+
func testCancellingPickerReturnsEmptyList() throws {
63+
let plugin = FileSelectorPlugin()
64+
let picker = UIDocumentPickerViewController(documentTypes: [], in: UIDocumentPickerMode.import)
65+
plugin.documentPickerViewControllerOverride = picker
66+
plugin.viewPresenterOverride = TestViewPresenter()
67+
let completionWasCalled = expectation(description: "completion")
68+
69+
plugin.openFile(
70+
config: FileSelectorConfig(utis: [], allowMultiSelection: false)
71+
) { result in
72+
switch result {
73+
case .success(let paths):
74+
XCTAssertEqual(paths.count, 0)
75+
case .failure(let error):
76+
XCTFail("\(error)")
77+
}
78+
completionWasCalled.fulfill()
79+
}
80+
plugin.pendingCompletions.first!.documentPickerWasCancelled(picker)
81+
82+
waitForExpectations(timeout: 30.0)
83+
XCTAssertTrue(plugin.pendingCompletions.isEmpty)
84+
}
85+
}

packages/file_selector/file_selector_ios/ios/file_selector_ios.podspec

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@ Displays the native iOS document picker.
1313
s.license = { :type => 'BSD', :file => '../LICENSE' }
1414
s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' }
1515
s.source = { :http => 'https://github.com/flutter/packages/tree/main/packages/file_selector/file_selector_ios' }
16-
s.source_files = 'file_selector_ios/Sources/file_selector_ios/**/*.{h,m}'
17-
s.module_map = 'file_selector_ios/Sources/file_selector_ios/include/cocoapods_file_selector_ios.modulemap'
16+
s.source_files = 'file_selector_ios/Sources/file_selector_ios/**/*.swift'
1817
s.dependency 'Flutter'
1918
s.platform = :ios, '12.0'
2019
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
2120
s.swift_version = '5.0'
21+
s.xcconfig = {
22+
'LIBRARY_SEARCH_PATHS' => '$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)/ $(SDKROOT)/usr/lib/swift',
23+
'LD_RUNPATH_SEARCH_PATHS' => '/usr/lib/swift',
24+
}
2225
s.resource_bundles = {'file_selector_ios_privacy' => ['file_selector_ios/Sources/file_selector_ios/Resources/PrivacyInfo.xcprivacy']}
2326
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// swift-tools-version: 5.9
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
// Copyright 2013 The Flutter Authors. All rights reserved.
5+
// Use of this source code is governed by a BSD-style license that can be
6+
// found in the LICENSE file.
7+
8+
import PackageDescription
9+
10+
let package = Package(
11+
name: "file_selector_ios",
12+
platforms: [
13+
.iOS("12.0")
14+
],
15+
products: [
16+
.library(name: "file-selector-ios", targets: ["file_selector_ios"])
17+
],
18+
dependencies: [],
19+
targets: [
20+
.target(
21+
name: "file_selector_ios",
22+
dependencies: [],
23+
resources: [
24+
.process("Resources")
25+
],
26+
cSettings: [
27+
.headerSearchPath("include/file_selector_ios")
28+
]
29+
)
30+
]
31+
)

0 commit comments

Comments
 (0)