├── .gitignore
├── Tests
├── LinuxMain.swift
└── FilePickerTests
│ ├── XCTestManifests.swift
│ └── FilePickerTests.swift
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENCE
├── Package.swift
├── README.md
└── Sources
└── FilePicker
├── FilePickerUIRepresentable.swift
└── FilePicker.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import FilePickerTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += FilePickerTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Tests/FilePickerTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(FilePickerTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Tests/FilePickerTests/FilePickerTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import FilePicker
3 |
4 | final class FilePickerTests: XCTestCase {
5 | func testExample() {
6 | // This is an example of a functional test case.
7 | // Use XCTAssert and related functions to verify your tests produce the correct
8 | // results.
9 | XCTAssertEqual(FilePicker().text, "Hello, World!")
10 | }
11 |
12 | static var allTests = [
13 | ("testExample", testExample),
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Mark Renaud
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.3
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "FilePicker",
8 | platforms: [
9 | .macOS(.v11),
10 | .iOS(.v14)
11 | ],
12 | products: [
13 | // Products define the executables and libraries a package produces, and make them visible to other packages.
14 | .library(
15 | name: "FilePicker",
16 | targets: ["FilePicker"]),
17 | ],
18 | dependencies: [
19 | // Dependencies declare other packages that this package depends on.
20 | // .package(url: /* package url */, from: "1.0.0"),
21 | ],
22 | targets: [
23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
24 | // Targets can depend on other targets in this package, and on products in packages this package depends on.
25 | .target(
26 | name: "FilePicker",
27 | dependencies: []),
28 | .testTarget(
29 | name: "FilePickerTests",
30 | dependencies: ["FilePicker"]),
31 | ]
32 | )
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # FilePicker
2 | > A cross platform file picker for SwiftUI.
3 |
4 | [![License][license-image]][license-url]
5 |
6 | The `FilePicker` package implements a SwiftUI view that can be used in **both** iOS and macOS.
7 | The `FilePicker` provides a `Button` that presents a platform-native file picker that is a modern way for use in SwiftUI.
8 |
9 | ## Installation
10 |
11 | Add the `FilePicker` package to your Xcode project using the package manager.
12 | Import `FilePicker` to your file.
13 |
14 | If you are using the package with macOS, ensure that you grant appropriate `File Access` permissions for `User Selected File` under `Signing & Capabilities` (on the macOS target)
15 |
16 | ## Example Usage
17 |
18 | ```swift
19 | import SwiftUI
20 | import FilePicker
21 |
22 | struct DebugView: View {
23 |
24 | var body: some View {
25 | HStack {
26 | //
27 |
28 | // Use custom content for the button label
29 | FilePicker(types: [.plainText], allowMultiple: true) { urls in
30 | print("selected \(urls.count) files")
31 | } label: {
32 | HStack {
33 | Image(systemName: "doc.on.doc")
34 | Text("Pick Files")
35 | }
36 | }
37 |
38 | FilePicker(types: [.plainText], allowMultiple: false, title: "pick single file") { urls in
39 | print("selected \(urls.count) files")
40 | }
41 |
42 | }
43 | }
44 |
45 | }
46 | ```
47 |
48 | ## Licence
49 | Distributed under the MIT license. See ``LICENSE`` for more information.
50 |
51 | [license-image]: https://img.shields.io/badge/License-MIT-blue.svg
52 | [license-url]: LICENSE
53 |
--------------------------------------------------------------------------------
/Sources/FilePicker/FilePickerUIRepresentable.swift:
--------------------------------------------------------------------------------
1 | // FilePicker Package
2 | //
3 | // MIT License
4 | //
5 | // Copyright (c) 2021 Mark Renaud
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in all
15 | // copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | // SOFTWARE.
24 |
25 | import SwiftUI
26 | import UniformTypeIdentifiers
27 |
28 | #if os(iOS)
29 |
30 | public struct FilePickerUIRepresentable: UIViewControllerRepresentable {
31 | public typealias UIViewControllerType = UIDocumentPickerViewController
32 | public typealias PickedURLsCompletionHandler = (_ urls: [URL]) -> Void
33 |
34 | @Environment(\.presentationMode) var presentationMode
35 |
36 | public let types: [UTType]
37 | public let allowMultiple: Bool
38 | public let pickedCompletionHandler: PickedURLsCompletionHandler
39 |
40 | public init(types: [UTType], allowMultiple: Bool, onPicked completionHandler: @escaping PickedURLsCompletionHandler) {
41 | self.types = types
42 | self.allowMultiple = allowMultiple
43 | self.pickedCompletionHandler = completionHandler
44 | }
45 |
46 | public func makeCoordinator() -> Coordinator {
47 | Coordinator(parent: self)
48 | }
49 |
50 | public func makeUIViewController(context: Context) -> UIDocumentPickerViewController {
51 | let picker = UIDocumentPickerViewController(forOpeningContentTypes: types, asCopy: true)
52 | picker.delegate = context.coordinator
53 | picker.allowsMultipleSelection = allowMultiple
54 | return picker
55 | }
56 |
57 | public func updateUIViewController(_ controller: UIDocumentPickerViewController, context: Context) {}
58 |
59 | public class Coordinator: NSObject, UIDocumentPickerDelegate {
60 | var parent: FilePickerUIRepresentable
61 |
62 | init(parent: FilePickerUIRepresentable) {
63 | self.parent = parent
64 | }
65 |
66 | public func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
67 | parent.pickedCompletionHandler(urls)
68 | parent.presentationMode.wrappedValue.dismiss()
69 | }
70 | }
71 | }
72 |
73 | #endif
74 |
--------------------------------------------------------------------------------
/Sources/FilePicker/FilePicker.swift:
--------------------------------------------------------------------------------
1 | // FilePicker.swift
2 | //
3 | // MIT License
4 | //
5 | // Copyright (c) 2021 Mark Renaud
6 | //
7 | // Permission is hereby granted, free of charge, to any person obtaining a copy
8 | // of this software and associated documentation files (the "Software"), to deal
9 | // in the Software without restriction, including without limitation the rights
10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | // copies of the Software, and to permit persons to whom the Software is
12 | // furnished to do so, subject to the following conditions:
13 | //
14 | // The above copyright notice and this permission notice shall be included in all
15 | // copies or substantial portions of the Software.
16 | //
17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23 | // SOFTWARE.
24 |
25 | import SwiftUI
26 | import UniformTypeIdentifiers
27 |
28 | public struct FilePicker: View {
29 |
30 | public typealias PickedURLsCompletionHandler = (_ urls: [URL]) -> Void
31 | public typealias LabelViewContent = () -> LabelView
32 |
33 | @State private var isPresented: Bool = false
34 |
35 | public let types: [UTType]
36 | public let allowMultiple: Bool
37 | public let pickedCompletionHandler: PickedURLsCompletionHandler
38 | public let labelViewContent: LabelViewContent
39 |
40 | public init(types: [UTType], allowMultiple: Bool, onPicked completionHandler: @escaping PickedURLsCompletionHandler, @ViewBuilder label labelViewContent: @escaping LabelViewContent) {
41 | self.types = types
42 | self.allowMultiple = allowMultiple
43 | self.pickedCompletionHandler = completionHandler
44 | self.labelViewContent = labelViewContent
45 | }
46 |
47 | public init(types: [UTType], allowMultiple: Bool, title: String, onPicked completionHandler: @escaping PickedURLsCompletionHandler) where LabelView == Text {
48 | self.init(types: types, allowMultiple: allowMultiple, onPicked: completionHandler) { Text(title) }
49 | }
50 |
51 | #if os(iOS)
52 |
53 | public var body: some View {
54 | Button(
55 | action: {
56 | if !isPresented { isPresented = true }
57 | },
58 | label: {
59 | labelViewContent()
60 | }
61 | )
62 | .disabled(isPresented)
63 | .sheet(isPresented: $isPresented) {
64 | FilePickerUIRepresentable(types: types, allowMultiple: allowMultiple, onPicked: pickedCompletionHandler)
65 | }
66 | }
67 |
68 | #endif
69 |
70 | #if os(macOS)
71 | public var body: some View {
72 | Button(
73 | action: {
74 | if !isPresented { isPresented = true }
75 | },
76 | label: {
77 | labelViewContent()
78 | }
79 | )
80 | .disabled(isPresented)
81 | .onChange(of: isPresented, perform: { presented in
82 | // binding changed from false to true
83 | if presented == true {
84 | let panel = NSOpenPanel()
85 | panel.allowsMultipleSelection = allowMultiple
86 | panel.canChooseDirectories = false
87 | panel.canChooseFiles = true
88 | panel.allowedFileTypes = types.map { $0.identifier }
89 | panel.begin { reponse in
90 | if reponse == .OK {
91 | pickedCompletionHandler(panel.urls)
92 | }
93 | // reset the isPresented variable to false
94 | isPresented = false
95 | }
96 | }
97 | })
98 | }
99 | #endif
100 |
101 | }
102 |
--------------------------------------------------------------------------------