├── .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 | --------------------------------------------------------------------------------