├── .gitignore ├── Demo ├── MediaPickerDemo.swiftpm │ ├── .swiftpm │ │ ├── playgrounds │ │ │ ├── CachedManifest.plist │ │ │ ├── DocumentThumbnail.plist │ │ │ ├── DocumentThumbnail.png │ │ │ └── Workspace.plist │ │ └── xcode │ │ │ └── package.xcworkspace │ │ │ └── contents.xcworkspacedata │ ├── App │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── PlaceholderAppIcon-images-1024.png │ │ │ │ ├── PlaceholderAppIcon-images-60@2x.png │ │ │ │ ├── PlaceholderAppIcon-images-60@3x.png │ │ │ │ ├── PlaceholderAppIcon-images-76@2x.png │ │ │ │ └── PlaceholderAppIcon-images-83.5@2x.png │ │ │ └── Contents.json │ │ ├── ContentView.swift │ │ └── MyApp.swift │ ├── License.txt │ ├── Package.resolved │ └── Package.swift ├── MediaPickerDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Shared │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── ContentView.swift │ ├── MediaPickerDemoApp.swift │ └── NativeImage.swift └── macOS │ └── macOS.entitlements ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── MediaPicker │ ├── MediaOptions.swift │ ├── MediaPicker-PhotosUI.swift │ ├── MediaPicker-iOS.swift │ ├── MediaPicker-macOS.swift │ └── MediaPickerErrors.swift └── Tests └── MediaPickerTests └── MediaPickerTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .swiftpm/* 50 | !.swiftpm/playgrounds/ 51 | .build/ 52 | 53 | # CocoaPods 54 | # 55 | # We recommend against adding the Pods directory to your .gitignore. However 56 | # you should judge for yourself, the pros and cons are mentioned at: 57 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 58 | # 59 | # Pods/ 60 | # 61 | # Add this line if you want to avoid checking in source code from the Xcode workspace 62 | # *.xcworkspace 63 | 64 | # Carthage 65 | # 66 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 67 | # Carthage/Checkouts 68 | 69 | Carthage/Build/ 70 | 71 | # Accio dependency management 72 | Dependencies/ 73 | .accio/ 74 | 75 | # fastlane 76 | # 77 | # It is recommended to not store the screenshots in the git repo. 78 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 79 | # For more information about the recommended setup visit: 80 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 81 | 82 | fastlane/report.xml 83 | fastlane/Preview.html 84 | fastlane/screenshots/**/*.png 85 | fastlane/test_output 86 | 87 | # Code Injection 88 | # 89 | # After new code Injection tools there's a generated folder /iOSInjectionProject 90 | # https://github.com/johnno1962/injectionforxcode 91 | 92 | iOSInjectionProject/ 93 | 94 | .DS_Store 95 | -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/.swiftpm/playgrounds/CachedManifest.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CachedManifest 6 | 7 | manifestData 8 | 9 | eyJkZXBlbmRlbmNpZXMiOlt7InNjbSI6W3siaWRlbnRpdHkiOiJzd2lmdHVp 10 | LW1lZGlhcGlja2VyIiwibG9jYXRpb24iOiJodHRwczpcL1wvZ2l0aHViLmNv 11 | bVwvVVdBcHBEZXZcL1N3aWZ0VUktTWVkaWFQaWNrZXIuZ2l0IiwicHJvZHVj 12 | dEZpbHRlciI6bnVsbCwicmVxdWlyZW1lbnQiOnsicmFuZ2UiOlt7Imxvd2Vy 13 | Qm91bmQiOiIwLjAuMSIsInVwcGVyQm91bmQiOiIxLjAuMCJ9XX19XX1dLCJu 14 | YW1lIjoiTWVkaWFQaWNrZXJEZW1vIiwicGFja2FnZUtpbmQiOiJyb290Iiwi 15 | cGxhdGZvcm1zIjpbeyJvcHRpb25zIjpbXSwicGxhdGZvcm1OYW1lIjoiaW9z 16 | IiwidmVyc2lvbiI6IjE1LjIifV0sInByb2R1Y3RzIjpbeyJuYW1lIjoiTWVk 17 | aWFQaWNrZXJEZW1vIiwic2V0dGluZ3MiOlt7ImJ1bmRsZUlkZW50aWZpZXIi 18 | OlsiZGV2LnV3YXBwLk1lZGlhUGlja2VyLkRlbW8iXX0seyJ0ZWFtSWRlbnRp 19 | ZmllciI6WyIySDg2NkYyMlc3Il19LHsiZGlzcGxheVZlcnNpb24iOlsiMS4w 20 | LjEiXX0seyJidW5kbGVWZXJzaW9uIjpbIjEiXX0seyJpT1NBcHBJbmZvIjpb 21 | eyJhY2NlbnRDb2xvckFzc2V0TmFtZSI6IkFjY2VudENvbG9yIiwiY2FwYWJp 22 | bGl0aWVzIjpbXSwiaWNvbkFzc2V0TmFtZSI6IkFwcEljb24iLCJzdXBwb3J0 23 | ZWREZXZpY2VGYW1pbGllcyI6WyJwYWQiLCJwaG9uZSJdLCJzdXBwb3J0ZWRJ 24 | bnRlcmZhY2VPcmllbnRhdGlvbnMiOlt7InBvcnRyYWl0Ijp7fX0seyJsYW5k 25 | c2NhcGVSaWdodCI6e319LHsibGFuZHNjYXBlTGVmdCI6e319LHsicG9ydHJh 26 | aXRVcHNpZGVEb3duIjp7ImNvbmRpdGlvbiI6eyJkZXZpY2VGYW1pbGllcyI6 27 | WyJwYWQiXX19fV19XX1dLCJ0YXJnZXRzIjpbIkFwcE1vZHVsZSJdLCJ0eXBl 28 | Ijp7ImV4ZWN1dGFibGUiOm51bGx9fV0sInRhcmdldE1hcCI6eyJBcHBNb2R1 29 | bGUiOnsiZGVwZW5kZW5jaWVzIjpbeyJwcm9kdWN0IjpbIk1lZGlhUGlja2Vy 30 | Iiwic3dpZnR1aS1tZWRpYXBpY2tlciIsbnVsbF19XSwiZXhjbHVkZSI6W10s 31 | Im5hbWUiOiJBcHBNb2R1bGUiLCJwYXRoIjoiQXBwIiwicmVzb3VyY2VzIjpb 32 | XSwic2V0dGluZ3MiOltdLCJ0eXBlIjoiZXhlY3V0YWJsZSJ9fSwidGFyZ2V0 33 | cyI6W3siZGVwZW5kZW5jaWVzIjpbeyJwcm9kdWN0IjpbIk1lZGlhUGlja2Vy 34 | Iiwic3dpZnR1aS1tZWRpYXBpY2tlciIsbnVsbF19XSwiZXhjbHVkZSI6W10s 35 | Im5hbWUiOiJBcHBNb2R1bGUiLCJwYXRoIjoiQXBwIiwicmVzb3VyY2VzIjpb 36 | XSwic2V0dGluZ3MiOltdLCJ0eXBlIjoiZXhlY3V0YWJsZSJ9XSwidG9vbHNW 37 | ZXJzaW9uIjp7Il92ZXJzaW9uIjoiNS41LjAifX0= 38 | 39 | manifestHash 40 | 41 | TUtxSwwY7XSia8Kdh/KVbSjX/aguNuSGbe8v56xeShQ= 42 | 43 | schemaVersion 44 | 3 45 | swiftPMVersionString 46 | 5.5.0 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | DocumentThumbnailConfiguration 6 | 7 | accentColorHash 8 | 9 | G0yRM9pzpxEyJAQxRAJ2WrDSP9NioWfW8MZbshURPZQ= 10 | 11 | appIconHash 12 | 13 | IbLu0eMoosYv5MF9URiL3qc0UPKZVtxcjJVCkxPd1yw= 14 | 15 | thumbnailIsPrerendered 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWAppDev/SwiftUI-MediaPicker/0e9342d80e944aa16768671b809186b9532ebe20/Demo/MediaPickerDemo.swiftpm/.swiftpm/playgrounds/DocumentThumbnail.png -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/.swiftpm/playgrounds/Workspace.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AppSettings 6 | 7 | appIconPlaceholderGlyphName 8 | images 9 | appSettingsVersion 10 | 1 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/App/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "universal", 6 | "reference" : "systemOrangeColor" 7 | }, 8 | "idiom" : "universal" 9 | } 10 | ], 11 | "info" : { 12 | "author" : "xcode", 13 | "version" : 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "PlaceholderAppIcon-images-60@2x.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "60x60" 8 | }, 9 | { 10 | "filename" : "PlaceholderAppIcon-images-60@3x.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "60x60" 14 | }, 15 | { 16 | "filename" : "PlaceholderAppIcon-images-76@2x.png", 17 | "idiom" : "ipad", 18 | "scale" : "2x", 19 | "size" : "76x76" 20 | }, 21 | { 22 | "filename" : "PlaceholderAppIcon-images-83.5@2x.png", 23 | "idiom" : "ipad", 24 | "scale" : "2x", 25 | "size" : "83.5x83.5" 26 | }, 27 | { 28 | "filename" : "PlaceholderAppIcon-images-1024.png", 29 | "idiom" : "ios-marketing", 30 | "scale" : "1x", 31 | "size" : "1024x1024" 32 | } 33 | ], 34 | "info" : { 35 | "author" : "xcode", 36 | "version" : 1 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/App/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-images-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWAppDev/SwiftUI-MediaPicker/0e9342d80e944aa16768671b809186b9532ebe20/Demo/MediaPickerDemo.swiftpm/App/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-images-1024.png -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/App/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-images-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWAppDev/SwiftUI-MediaPicker/0e9342d80e944aa16768671b809186b9532ebe20/Demo/MediaPickerDemo.swiftpm/App/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-images-60@2x.png -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/App/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-images-60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWAppDev/SwiftUI-MediaPicker/0e9342d80e944aa16768671b809186b9532ebe20/Demo/MediaPickerDemo.swiftpm/App/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-images-60@3x.png -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/App/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-images-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWAppDev/SwiftUI-MediaPicker/0e9342d80e944aa16768671b809186b9532ebe20/Demo/MediaPickerDemo.swiftpm/App/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-images-76@2x.png -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/App/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-images-83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UWAppDev/SwiftUI-MediaPicker/0e9342d80e944aa16768671b809186b9532ebe20/Demo/MediaPickerDemo.swiftpm/App/Assets.xcassets/AppIcon.appiconset/PlaceholderAppIcon-images-83.5@2x.png -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/App/ContentView.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftUI FoodTracker tutorial series 4 | // 5 | // Copyright (c) 2020-2022 AppDev@UW.edu and the SwiftUI MediaPicker authors 6 | // Licensed under MIT License 7 | // 8 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/blob/main/LICENSE 9 | // for license information 10 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/graphs/contributors 11 | // for the list of SwiftUI MediaPicker project authors 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import SwiftUI 16 | import AVKit 17 | import MediaPicker 18 | 19 | struct ContentView: View { 20 | @State var urls: [URL] = [] 21 | @State var isShowingMediaPicker = false 22 | 23 | var selectButton: some View { 24 | Button("Select Media") { 25 | isShowingMediaPicker = true 26 | } 27 | .mediaImporter(isPresented: $isShowingMediaPicker, 28 | allowedMediaTypes: .all, 29 | allowsMultipleSelection: true) { result in 30 | switch result { 31 | case .success(let urls): 32 | self.urls = urls 33 | case .failure(let error): 34 | print(error) 35 | self.urls = [] 36 | } 37 | } 38 | } 39 | 40 | var body: some View { 41 | List { 42 | Section { 43 | ForEach(urls, id: \.absoluteString) { url in 44 | switch try! url.resourceValues(forKeys: [.contentTypeKey]).contentType! { 45 | case let contentType where contentType.conforms(to: .image): 46 | AsyncImage(url: url) { image in 47 | image 48 | .resizable() 49 | .scaledToFit() 50 | } placeholder: { 51 | ProgressView() 52 | } 53 | case let contentType where contentType.conforms(to: .audiovisualContent): 54 | VideoPlayer(player: AVPlayer(url: url)) 55 | .scaledToFit() 56 | default: 57 | Text("Can't display \(url.lastPathComponent)") 58 | } 59 | } 60 | } header: { 61 | selectButton 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/App/MyApp.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftUI FoodTracker tutorial series 4 | // 5 | // Copyright (c) 2020-2022 AppDev@UW.edu and the SwiftUI MediaPicker authors 6 | // Licensed under MIT License 7 | // 8 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/blob/main/LICENSE 9 | // for license information 10 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/graphs/contributors 11 | // for the list of SwiftUI MediaPicker project authors 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import SwiftUI 16 | 17 | @main 18 | struct MyApp: App { 19 | var body: some Scene { 20 | WindowGroup { 21 | ContentView() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2022 AppDev@UW.edu and the SwiftUI MediaPicker authors. 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 | -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "SwiftUI-MediaPicker", 6 | "repositoryURL": "https://github.com/UWAppDev/SwiftUI-MediaPicker.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "8c6c7f3acbe4df234b383cfaac5996945b547aab", 10 | "version": "0.1.0" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.swiftpm/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.5 2 | 3 | // WARNING: 4 | // This file is automatically generated. 5 | // Do not edit it by hand because the contents will be replaced. 6 | 7 | import PackageDescription 8 | import AppleProductTypes 9 | 10 | let package = Package( 11 | name: "SwiftUI Media Picker", 12 | platforms: [ 13 | .iOS("15.2") 14 | ], 15 | products: [ 16 | .iOSApplication( 17 | name: "SwiftUI Media Picker", 18 | targets: ["AppModule"], 19 | bundleIdentifier: "dev.uwapp.MediaPicker.Demo", 20 | teamIdentifier: "2H866F22W7", 21 | displayVersion: "1.0.2", 22 | bundleVersion: "1", 23 | iconAssetName: "AppIcon", 24 | accentColorAssetName: "AccentColor", 25 | supportedDeviceFamilies: [ 26 | .pad, 27 | .phone 28 | ], 29 | supportedInterfaceOrientations: [ 30 | .portrait, 31 | .landscapeRight, 32 | .landscapeLeft, 33 | .portraitUpsideDown(.when(deviceFamilies: [.pad])) 34 | ] 35 | ) 36 | ], 37 | dependencies: [ 38 | .package(url: "https://github.com/UWAppDev/SwiftUI-MediaPicker.git", "0.0.1"..<"1.0.0") 39 | ], 40 | targets: [ 41 | .executableTarget( 42 | name: "AppModule", 43 | dependencies: [ 44 | .product(name: "MediaPicker", package: "swiftui-mediapicker") 45 | ], 46 | path: "App" 47 | ) 48 | ] 49 | ) -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | D2A0C5F92768268E00CB4F6F /* MediaPickerDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A0C5E92768268C00CB4F6F /* MediaPickerDemoApp.swift */; }; 11 | D2A0C5FA2768268E00CB4F6F /* MediaPickerDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A0C5E92768268C00CB4F6F /* MediaPickerDemoApp.swift */; }; 12 | D2A0C5FB2768268E00CB4F6F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A0C5EA2768268C00CB4F6F /* ContentView.swift */; }; 13 | D2A0C5FC2768268E00CB4F6F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A0C5EA2768268C00CB4F6F /* ContentView.swift */; }; 14 | D2A0C5FD2768268E00CB4F6F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D2A0C5EB2768268D00CB4F6F /* Assets.xcassets */; }; 15 | D2A0C5FE2768268E00CB4F6F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D2A0C5EB2768268D00CB4F6F /* Assets.xcassets */; }; 16 | D2A0C60A276826B400CB4F6F /* MediaPicker in Frameworks */ = {isa = PBXBuildFile; productRef = D2A0C609276826B400CB4F6F /* MediaPicker */; }; 17 | D2A0C60C276826B900CB4F6F /* MediaPicker in Frameworks */ = {isa = PBXBuildFile; productRef = D2A0C60B276826B900CB4F6F /* MediaPicker */; }; 18 | D2A0C60F2768281300CB4F6F /* NativeImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A0C60E2768281300CB4F6F /* NativeImage.swift */; }; 19 | D2A0C6102768281300CB4F6F /* NativeImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2A0C60E2768281300CB4F6F /* NativeImage.swift */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXFileReference section */ 23 | D2A0C5E92768268C00CB4F6F /* MediaPickerDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerDemoApp.swift; sourceTree = ""; }; 24 | D2A0C5EA2768268C00CB4F6F /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 25 | D2A0C5EB2768268D00CB4F6F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 26 | D2A0C5F02768268D00CB4F6F /* MediaPickerDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MediaPickerDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | D2A0C5F62768268E00CB4F6F /* MediaPickerDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MediaPickerDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 28 | D2A0C5F82768268E00CB4F6F /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; }; 29 | D2A0C607276826AA00CB4F6F /* SwiftUI-MediaPicker */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "SwiftUI-MediaPicker"; path = ..; sourceTree = ""; }; 30 | D2A0C60D276826BF00CB4F6F /* MediaPickerDemo.swiftpm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MediaPickerDemo.swiftpm; sourceTree = ""; }; 31 | D2A0C60E2768281300CB4F6F /* NativeImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeImage.swift; sourceTree = ""; }; 32 | /* End PBXFileReference section */ 33 | 34 | /* Begin PBXFrameworksBuildPhase section */ 35 | D2A0C5ED2768268D00CB4F6F /* Frameworks */ = { 36 | isa = PBXFrameworksBuildPhase; 37 | buildActionMask = 2147483647; 38 | files = ( 39 | D2A0C60A276826B400CB4F6F /* MediaPicker in Frameworks */, 40 | ); 41 | runOnlyForDeploymentPostprocessing = 0; 42 | }; 43 | D2A0C5F32768268D00CB4F6F /* Frameworks */ = { 44 | isa = PBXFrameworksBuildPhase; 45 | buildActionMask = 2147483647; 46 | files = ( 47 | D2A0C60C276826B900CB4F6F /* MediaPicker in Frameworks */, 48 | ); 49 | runOnlyForDeploymentPostprocessing = 0; 50 | }; 51 | /* End PBXFrameworksBuildPhase section */ 52 | 53 | /* Begin PBXGroup section */ 54 | D2A0C5E32768268C00CB4F6F = { 55 | isa = PBXGroup; 56 | children = ( 57 | D2A0C607276826AA00CB4F6F /* SwiftUI-MediaPicker */, 58 | D2A0C60D276826BF00CB4F6F /* MediaPickerDemo.swiftpm */, 59 | D2A0C5E82768268C00CB4F6F /* Shared */, 60 | D2A0C5F72768268E00CB4F6F /* macOS */, 61 | D2A0C5F12768268D00CB4F6F /* Products */, 62 | D2A0C608276826B400CB4F6F /* Frameworks */, 63 | ); 64 | sourceTree = ""; 65 | }; 66 | D2A0C5E82768268C00CB4F6F /* Shared */ = { 67 | isa = PBXGroup; 68 | children = ( 69 | D2A0C5EB2768268D00CB4F6F /* Assets.xcassets */, 70 | D2A0C5EA2768268C00CB4F6F /* ContentView.swift */, 71 | D2A0C5E92768268C00CB4F6F /* MediaPickerDemoApp.swift */, 72 | D2A0C60E2768281300CB4F6F /* NativeImage.swift */, 73 | ); 74 | path = Shared; 75 | sourceTree = ""; 76 | }; 77 | D2A0C5F12768268D00CB4F6F /* Products */ = { 78 | isa = PBXGroup; 79 | children = ( 80 | D2A0C5F02768268D00CB4F6F /* MediaPickerDemo.app */, 81 | D2A0C5F62768268E00CB4F6F /* MediaPickerDemo.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | D2A0C5F72768268E00CB4F6F /* macOS */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | D2A0C5F82768268E00CB4F6F /* macOS.entitlements */, 90 | ); 91 | path = macOS; 92 | sourceTree = ""; 93 | }; 94 | D2A0C608276826B400CB4F6F /* Frameworks */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | ); 98 | name = Frameworks; 99 | sourceTree = ""; 100 | }; 101 | /* End PBXGroup section */ 102 | 103 | /* Begin PBXNativeTarget section */ 104 | D2A0C5EF2768268D00CB4F6F /* MediaPickerDemo (iOS) */ = { 105 | isa = PBXNativeTarget; 106 | buildConfigurationList = D2A0C6012768268E00CB4F6F /* Build configuration list for PBXNativeTarget "MediaPickerDemo (iOS)" */; 107 | buildPhases = ( 108 | D2A0C5EC2768268D00CB4F6F /* Sources */, 109 | D2A0C5ED2768268D00CB4F6F /* Frameworks */, 110 | D2A0C5EE2768268D00CB4F6F /* Resources */, 111 | ); 112 | buildRules = ( 113 | ); 114 | dependencies = ( 115 | ); 116 | name = "MediaPickerDemo (iOS)"; 117 | packageProductDependencies = ( 118 | D2A0C609276826B400CB4F6F /* MediaPicker */, 119 | ); 120 | productName = "MediaPickerDemo (iOS)"; 121 | productReference = D2A0C5F02768268D00CB4F6F /* MediaPickerDemo.app */; 122 | productType = "com.apple.product-type.application"; 123 | }; 124 | D2A0C5F52768268D00CB4F6F /* MediaPickerDemo (macOS) */ = { 125 | isa = PBXNativeTarget; 126 | buildConfigurationList = D2A0C6042768268E00CB4F6F /* Build configuration list for PBXNativeTarget "MediaPickerDemo (macOS)" */; 127 | buildPhases = ( 128 | D2A0C5F22768268D00CB4F6F /* Sources */, 129 | D2A0C5F32768268D00CB4F6F /* Frameworks */, 130 | D2A0C5F42768268D00CB4F6F /* Resources */, 131 | ); 132 | buildRules = ( 133 | ); 134 | dependencies = ( 135 | ); 136 | name = "MediaPickerDemo (macOS)"; 137 | packageProductDependencies = ( 138 | D2A0C60B276826B900CB4F6F /* MediaPicker */, 139 | ); 140 | productName = "MediaPickerDemo (macOS)"; 141 | productReference = D2A0C5F62768268E00CB4F6F /* MediaPickerDemo.app */; 142 | productType = "com.apple.product-type.application"; 143 | }; 144 | /* End PBXNativeTarget section */ 145 | 146 | /* Begin PBXProject section */ 147 | D2A0C5E42768268C00CB4F6F /* Project object */ = { 148 | isa = PBXProject; 149 | attributes = { 150 | BuildIndependentTargetsInParallel = 1; 151 | LastSwiftUpdateCheck = 1320; 152 | LastUpgradeCheck = 1320; 153 | TargetAttributes = { 154 | D2A0C5EF2768268D00CB4F6F = { 155 | CreatedOnToolsVersion = 13.2; 156 | }; 157 | D2A0C5F52768268D00CB4F6F = { 158 | CreatedOnToolsVersion = 13.2; 159 | }; 160 | }; 161 | }; 162 | buildConfigurationList = D2A0C5E72768268C00CB4F6F /* Build configuration list for PBXProject "MediaPickerDemo" */; 163 | compatibilityVersion = "Xcode 13.0"; 164 | developmentRegion = en; 165 | hasScannedForEncodings = 0; 166 | knownRegions = ( 167 | en, 168 | Base, 169 | ); 170 | mainGroup = D2A0C5E32768268C00CB4F6F; 171 | productRefGroup = D2A0C5F12768268D00CB4F6F /* Products */; 172 | projectDirPath = ""; 173 | projectRoot = ""; 174 | targets = ( 175 | D2A0C5EF2768268D00CB4F6F /* MediaPickerDemo (iOS) */, 176 | D2A0C5F52768268D00CB4F6F /* MediaPickerDemo (macOS) */, 177 | ); 178 | }; 179 | /* End PBXProject section */ 180 | 181 | /* Begin PBXResourcesBuildPhase section */ 182 | D2A0C5EE2768268D00CB4F6F /* Resources */ = { 183 | isa = PBXResourcesBuildPhase; 184 | buildActionMask = 2147483647; 185 | files = ( 186 | D2A0C5FD2768268E00CB4F6F /* Assets.xcassets in Resources */, 187 | ); 188 | runOnlyForDeploymentPostprocessing = 0; 189 | }; 190 | D2A0C5F42768268D00CB4F6F /* Resources */ = { 191 | isa = PBXResourcesBuildPhase; 192 | buildActionMask = 2147483647; 193 | files = ( 194 | D2A0C5FE2768268E00CB4F6F /* Assets.xcassets in Resources */, 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | }; 198 | /* End PBXResourcesBuildPhase section */ 199 | 200 | /* Begin PBXSourcesBuildPhase section */ 201 | D2A0C5EC2768268D00CB4F6F /* Sources */ = { 202 | isa = PBXSourcesBuildPhase; 203 | buildActionMask = 2147483647; 204 | files = ( 205 | D2A0C5FB2768268E00CB4F6F /* ContentView.swift in Sources */, 206 | D2A0C5F92768268E00CB4F6F /* MediaPickerDemoApp.swift in Sources */, 207 | D2A0C60F2768281300CB4F6F /* NativeImage.swift in Sources */, 208 | ); 209 | runOnlyForDeploymentPostprocessing = 0; 210 | }; 211 | D2A0C5F22768268D00CB4F6F /* Sources */ = { 212 | isa = PBXSourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | D2A0C5FC2768268E00CB4F6F /* ContentView.swift in Sources */, 216 | D2A0C5FA2768268E00CB4F6F /* MediaPickerDemoApp.swift in Sources */, 217 | D2A0C6102768281300CB4F6F /* NativeImage.swift in Sources */, 218 | ); 219 | runOnlyForDeploymentPostprocessing = 0; 220 | }; 221 | /* End PBXSourcesBuildPhase section */ 222 | 223 | /* Begin XCBuildConfiguration section */ 224 | D2A0C5FF2768268E00CB4F6F /* Debug */ = { 225 | isa = XCBuildConfiguration; 226 | buildSettings = { 227 | ALWAYS_SEARCH_USER_PATHS = NO; 228 | CLANG_ANALYZER_NONNULL = YES; 229 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 230 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 231 | CLANG_CXX_LIBRARY = "libc++"; 232 | CLANG_ENABLE_MODULES = YES; 233 | CLANG_ENABLE_OBJC_ARC = YES; 234 | CLANG_ENABLE_OBJC_WEAK = YES; 235 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 236 | CLANG_WARN_BOOL_CONVERSION = YES; 237 | CLANG_WARN_COMMA = YES; 238 | CLANG_WARN_CONSTANT_CONVERSION = YES; 239 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 240 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 241 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 242 | CLANG_WARN_EMPTY_BODY = YES; 243 | CLANG_WARN_ENUM_CONVERSION = YES; 244 | CLANG_WARN_INFINITE_RECURSION = YES; 245 | CLANG_WARN_INT_CONVERSION = YES; 246 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 247 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 248 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 249 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 250 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 251 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 252 | CLANG_WARN_STRICT_PROTOTYPES = YES; 253 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 254 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 255 | CLANG_WARN_UNREACHABLE_CODE = YES; 256 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 257 | COPY_PHASE_STRIP = NO; 258 | DEBUG_INFORMATION_FORMAT = dwarf; 259 | ENABLE_STRICT_OBJC_MSGSEND = YES; 260 | ENABLE_TESTABILITY = YES; 261 | GCC_C_LANGUAGE_STANDARD = gnu11; 262 | GCC_DYNAMIC_NO_PIC = NO; 263 | GCC_NO_COMMON_BLOCKS = YES; 264 | GCC_OPTIMIZATION_LEVEL = 0; 265 | GCC_PREPROCESSOR_DEFINITIONS = ( 266 | "DEBUG=1", 267 | "$(inherited)", 268 | ); 269 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 270 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 271 | GCC_WARN_UNDECLARED_SELECTOR = YES; 272 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 273 | GCC_WARN_UNUSED_FUNCTION = YES; 274 | GCC_WARN_UNUSED_VARIABLE = YES; 275 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 276 | MTL_FAST_MATH = YES; 277 | ONLY_ACTIVE_ARCH = YES; 278 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 279 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 280 | }; 281 | name = Debug; 282 | }; 283 | D2A0C6002768268E00CB4F6F /* Release */ = { 284 | isa = XCBuildConfiguration; 285 | buildSettings = { 286 | ALWAYS_SEARCH_USER_PATHS = NO; 287 | CLANG_ANALYZER_NONNULL = YES; 288 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 289 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 290 | CLANG_CXX_LIBRARY = "libc++"; 291 | CLANG_ENABLE_MODULES = YES; 292 | CLANG_ENABLE_OBJC_ARC = YES; 293 | CLANG_ENABLE_OBJC_WEAK = YES; 294 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 295 | CLANG_WARN_BOOL_CONVERSION = YES; 296 | CLANG_WARN_COMMA = YES; 297 | CLANG_WARN_CONSTANT_CONVERSION = YES; 298 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 299 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 300 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 301 | CLANG_WARN_EMPTY_BODY = YES; 302 | CLANG_WARN_ENUM_CONVERSION = YES; 303 | CLANG_WARN_INFINITE_RECURSION = YES; 304 | CLANG_WARN_INT_CONVERSION = YES; 305 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 306 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 307 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 308 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 309 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 310 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 311 | CLANG_WARN_STRICT_PROTOTYPES = YES; 312 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 313 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 314 | CLANG_WARN_UNREACHABLE_CODE = YES; 315 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 316 | COPY_PHASE_STRIP = NO; 317 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 318 | ENABLE_NS_ASSERTIONS = NO; 319 | ENABLE_STRICT_OBJC_MSGSEND = YES; 320 | GCC_C_LANGUAGE_STANDARD = gnu11; 321 | GCC_NO_COMMON_BLOCKS = YES; 322 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 323 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 324 | GCC_WARN_UNDECLARED_SELECTOR = YES; 325 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 326 | GCC_WARN_UNUSED_FUNCTION = YES; 327 | GCC_WARN_UNUSED_VARIABLE = YES; 328 | MTL_ENABLE_DEBUG_INFO = NO; 329 | MTL_FAST_MATH = YES; 330 | SWIFT_COMPILATION_MODE = wholemodule; 331 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 332 | }; 333 | name = Release; 334 | }; 335 | D2A0C6022768268E00CB4F6F /* Debug */ = { 336 | isa = XCBuildConfiguration; 337 | buildSettings = { 338 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 339 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 340 | CODE_SIGN_STYLE = Automatic; 341 | CURRENT_PROJECT_VERSION = 1; 342 | DEVELOPMENT_TEAM = 2H866F22W7; 343 | ENABLE_PREVIEWS = YES; 344 | GENERATE_INFOPLIST_FILE = YES; 345 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 346 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 347 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 348 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 349 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 350 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 351 | LD_RUNPATH_SEARCH_PATHS = ( 352 | "$(inherited)", 353 | "@executable_path/Frameworks", 354 | ); 355 | MARKETING_VERSION = 1.0; 356 | PRODUCT_BUNDLE_IDENTIFIER = dev.uwapp.MediaPicker.Demo; 357 | PRODUCT_NAME = MediaPickerDemo; 358 | SDKROOT = iphoneos; 359 | SWIFT_EMIT_LOC_STRINGS = YES; 360 | SWIFT_VERSION = 5.0; 361 | TARGETED_DEVICE_FAMILY = "1,2"; 362 | }; 363 | name = Debug; 364 | }; 365 | D2A0C6032768268E00CB4F6F /* Release */ = { 366 | isa = XCBuildConfiguration; 367 | buildSettings = { 368 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 369 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 370 | CODE_SIGN_STYLE = Automatic; 371 | CURRENT_PROJECT_VERSION = 1; 372 | DEVELOPMENT_TEAM = 2H866F22W7; 373 | ENABLE_PREVIEWS = YES; 374 | GENERATE_INFOPLIST_FILE = YES; 375 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 376 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 377 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 378 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 379 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 380 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 381 | LD_RUNPATH_SEARCH_PATHS = ( 382 | "$(inherited)", 383 | "@executable_path/Frameworks", 384 | ); 385 | MARKETING_VERSION = 1.0; 386 | PRODUCT_BUNDLE_IDENTIFIER = dev.uwapp.MediaPicker.Demo; 387 | PRODUCT_NAME = MediaPickerDemo; 388 | SDKROOT = iphoneos; 389 | SWIFT_EMIT_LOC_STRINGS = YES; 390 | SWIFT_VERSION = 5.0; 391 | TARGETED_DEVICE_FAMILY = "1,2"; 392 | VALIDATE_PRODUCT = YES; 393 | }; 394 | name = Release; 395 | }; 396 | D2A0C6052768268E00CB4F6F /* Debug */ = { 397 | isa = XCBuildConfiguration; 398 | buildSettings = { 399 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 400 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 401 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; 402 | CODE_SIGN_STYLE = Automatic; 403 | COMBINE_HIDPI_IMAGES = YES; 404 | CURRENT_PROJECT_VERSION = 1; 405 | DEVELOPMENT_TEAM = 2H866F22W7; 406 | ENABLE_HARDENED_RUNTIME = YES; 407 | ENABLE_PREVIEWS = YES; 408 | GENERATE_INFOPLIST_FILE = YES; 409 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 410 | LD_RUNPATH_SEARCH_PATHS = ( 411 | "$(inherited)", 412 | "@executable_path/../Frameworks", 413 | ); 414 | MACOSX_DEPLOYMENT_TARGET = 11.0; 415 | MARKETING_VERSION = 1.0; 416 | PRODUCT_BUNDLE_IDENTIFIER = dev.uwapp.MediaPicker.Demo; 417 | PRODUCT_NAME = MediaPickerDemo; 418 | SDKROOT = macosx; 419 | SWIFT_EMIT_LOC_STRINGS = YES; 420 | SWIFT_VERSION = 5.0; 421 | }; 422 | name = Debug; 423 | }; 424 | D2A0C6062768268E00CB4F6F /* Release */ = { 425 | isa = XCBuildConfiguration; 426 | buildSettings = { 427 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 428 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 429 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; 430 | CODE_SIGN_STYLE = Automatic; 431 | COMBINE_HIDPI_IMAGES = YES; 432 | CURRENT_PROJECT_VERSION = 1; 433 | DEVELOPMENT_TEAM = 2H866F22W7; 434 | ENABLE_HARDENED_RUNTIME = YES; 435 | ENABLE_PREVIEWS = YES; 436 | GENERATE_INFOPLIST_FILE = YES; 437 | INFOPLIST_KEY_NSHumanReadableCopyright = ""; 438 | LD_RUNPATH_SEARCH_PATHS = ( 439 | "$(inherited)", 440 | "@executable_path/../Frameworks", 441 | ); 442 | MACOSX_DEPLOYMENT_TARGET = 11.0; 443 | MARKETING_VERSION = 1.0; 444 | PRODUCT_BUNDLE_IDENTIFIER = dev.uwapp.MediaPicker.Demo; 445 | PRODUCT_NAME = MediaPickerDemo; 446 | SDKROOT = macosx; 447 | SWIFT_EMIT_LOC_STRINGS = YES; 448 | SWIFT_VERSION = 5.0; 449 | }; 450 | name = Release; 451 | }; 452 | /* End XCBuildConfiguration section */ 453 | 454 | /* Begin XCConfigurationList section */ 455 | D2A0C5E72768268C00CB4F6F /* Build configuration list for PBXProject "MediaPickerDemo" */ = { 456 | isa = XCConfigurationList; 457 | buildConfigurations = ( 458 | D2A0C5FF2768268E00CB4F6F /* Debug */, 459 | D2A0C6002768268E00CB4F6F /* Release */, 460 | ); 461 | defaultConfigurationIsVisible = 0; 462 | defaultConfigurationName = Release; 463 | }; 464 | D2A0C6012768268E00CB4F6F /* Build configuration list for PBXNativeTarget "MediaPickerDemo (iOS)" */ = { 465 | isa = XCConfigurationList; 466 | buildConfigurations = ( 467 | D2A0C6022768268E00CB4F6F /* Debug */, 468 | D2A0C6032768268E00CB4F6F /* Release */, 469 | ); 470 | defaultConfigurationIsVisible = 0; 471 | defaultConfigurationName = Release; 472 | }; 473 | D2A0C6042768268E00CB4F6F /* Build configuration list for PBXNativeTarget "MediaPickerDemo (macOS)" */ = { 474 | isa = XCConfigurationList; 475 | buildConfigurations = ( 476 | D2A0C6052768268E00CB4F6F /* Debug */, 477 | D2A0C6062768268E00CB4F6F /* Release */, 478 | ); 479 | defaultConfigurationIsVisible = 0; 480 | defaultConfigurationName = Release; 481 | }; 482 | /* End XCConfigurationList section */ 483 | 484 | /* Begin XCSwiftPackageProductDependency section */ 485 | D2A0C609276826B400CB4F6F /* MediaPicker */ = { 486 | isa = XCSwiftPackageProductDependency; 487 | productName = MediaPicker; 488 | }; 489 | D2A0C60B276826B900CB4F6F /* MediaPicker */ = { 490 | isa = XCSwiftPackageProductDependency; 491 | productName = MediaPicker; 492 | }; 493 | /* End XCSwiftPackageProductDependency section */ 494 | }; 495 | rootObject = D2A0C5E42768268C00CB4F6F /* Project object */; 496 | } 497 | -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/MediaPickerDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | }, 93 | { 94 | "idiom" : "mac", 95 | "scale" : "1x", 96 | "size" : "16x16" 97 | }, 98 | { 99 | "idiom" : "mac", 100 | "scale" : "2x", 101 | "size" : "16x16" 102 | }, 103 | { 104 | "idiom" : "mac", 105 | "scale" : "1x", 106 | "size" : "32x32" 107 | }, 108 | { 109 | "idiom" : "mac", 110 | "scale" : "2x", 111 | "size" : "32x32" 112 | }, 113 | { 114 | "idiom" : "mac", 115 | "scale" : "1x", 116 | "size" : "128x128" 117 | }, 118 | { 119 | "idiom" : "mac", 120 | "scale" : "2x", 121 | "size" : "128x128" 122 | }, 123 | { 124 | "idiom" : "mac", 125 | "scale" : "1x", 126 | "size" : "256x256" 127 | }, 128 | { 129 | "idiom" : "mac", 130 | "scale" : "2x", 131 | "size" : "256x256" 132 | }, 133 | { 134 | "idiom" : "mac", 135 | "scale" : "1x", 136 | "size" : "512x512" 137 | }, 138 | { 139 | "idiom" : "mac", 140 | "scale" : "2x", 141 | "size" : "512x512" 142 | } 143 | ], 144 | "info" : { 145 | "author" : "xcode", 146 | "version" : 1 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /Demo/Shared/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Shared/ContentView.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftUI FoodTracker tutorial series 4 | // 5 | // Copyright (c) 2020-2022 AppDev@UW.edu and the SwiftUI MediaPicker authors 6 | // Licensed under MIT License 7 | // 8 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/blob/main/LICENSE 9 | // for license information 10 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/graphs/contributors 11 | // for the list of SwiftUI MediaPicker project authors 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import SwiftUI 16 | import AVKit 17 | import MediaPicker 18 | 19 | struct ContentView: View { 20 | @State var urls: [URL] = [] 21 | @State var isShowingMediaPicker = false 22 | 23 | var selectButton: some View { 24 | Button("Select Media") { 25 | isShowingMediaPicker = true 26 | } 27 | .mediaImporter(isPresented: $isShowingMediaPicker, 28 | allowedMediaTypes: .all, 29 | allowsMultipleSelection: true) { result in 30 | switch result { 31 | case .success(let urls): 32 | self.urls = urls 33 | case .failure(let error): 34 | switch error { 35 | case MediaPickerErrors.imageURL(let urls, errors: let errors): 36 | print(errors) 37 | self.urls = urls 38 | default: 39 | print(error) 40 | self.urls = [] 41 | } 42 | } 43 | } 44 | } 45 | 46 | var body: some View { 47 | List { 48 | Section { 49 | ForEach(urls, id: \.absoluteString) { url in 50 | switch try! url.resourceValues(forKeys: [.contentTypeKey]).contentType! { 51 | case let contentType where contentType.conforms(to: .image): 52 | if #available(iOS 15, macOS 12, *) { 53 | AsyncImage(url: url) { image in 54 | image 55 | .resizable() 56 | .scaledToFit() 57 | } placeholder: { 58 | ProgressView() 59 | } 60 | } else { 61 | if let image = NativeImage(contentsOfFile: url.path) { 62 | Image(nativeImage: image) 63 | .resizable() 64 | .scaledToFit() 65 | } else { 66 | Text("Can't load contents of \(url)") 67 | } 68 | } 69 | case let contentType where contentType.conforms(to: .audiovisualContent): 70 | VideoPlayer(player: AVPlayer(url: url)) 71 | .scaledToFit() 72 | default: 73 | Text("Can't display \(url.lastPathComponent)") 74 | } 75 | } 76 | } header: { 77 | selectButton 78 | } 79 | } 80 | } 81 | } 82 | 83 | struct ContentView_Previews: PreviewProvider { 84 | static var previews: some View { 85 | ContentView() 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Demo/Shared/MediaPickerDemoApp.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftUI FoodTracker tutorial series 4 | // 5 | // Copyright (c) 2020-2022 AppDev@UW.edu and the SwiftUI MediaPicker authors 6 | // Licensed under MIT License 7 | // 8 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/blob/main/LICENSE 9 | // for license information 10 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/graphs/contributors 11 | // for the list of SwiftUI MediaPicker project authors 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import SwiftUI 16 | 17 | @main 18 | struct MediaPickerDemoApp: App { 19 | var body: some Scene { 20 | WindowGroup { 21 | ContentView() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Demo/Shared/NativeImage.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftUI FoodTracker tutorial series 4 | // 5 | // Copyright (c) 2020-2022 AppDev@UW.edu and the SwiftUI MediaPicker authors 6 | // Licensed under MIT License 7 | // 8 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/blob/main/LICENSE 9 | // for license information 10 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/graphs/contributors 11 | // for the list of SwiftUI MediaPicker project authors 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import SwiftUI 16 | 17 | #if canImport(UIKit) 18 | import UIKit 19 | 20 | typealias NativeImage = UIImage 21 | 22 | extension Image { 23 | init(nativeImage: NativeImage) { 24 | self.init(uiImage: nativeImage) 25 | } 26 | } 27 | #else 28 | import AppKit 29 | 30 | typealias NativeImage = NSImage 31 | 32 | extension Image { 33 | init(nativeImage: NativeImage) { 34 | self.init(nsImage: nativeImage) 35 | } 36 | } 37 | #endif 38 | -------------------------------------------------------------------------------- /Demo/macOS/macOS.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2022 AppDev@UW.edu and the SwiftUI MediaPicker authors. 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.5 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: "SwiftUI-MediaPicker", 8 | platforms: [ 9 | .iOS(.v14), .macOS(.v11), 10 | ], 11 | products: [ 12 | .library( 13 | name: "MediaPicker", 14 | targets: ["MediaPicker"]), 15 | ], 16 | targets: [ 17 | .target( 18 | name: "MediaPicker", 19 | dependencies: []), 20 | .testTarget( 21 | name: "MediaPickerTests", 22 | dependencies: ["MediaPicker"]), 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SwiftUI Media Picker 2 | 3 | iOS/macOS media picker for importing images and videos in SwiftUI. 4 | 5 | ## Adding SwiftUI Media Picker as a Dependency 6 | 7 | 1. If you are using Xcode and have a minimum deployment target of iOS 14/macOS 11, make sure you are using Xcode 13.2 or later 8 | 2. Add the following line to the Package's `dependencies` array in your `Package.swift` file: 9 | 10 | ```swift 11 | .package(url: "https://github.com/UWAppDev/SwiftUI-MediaPicker", from: "0.2.0"), 12 | ``` 13 | 14 | 3. Include this library as a dependency for your executable target: 15 | 16 | ```swift 17 | .target(name: "", dependencies: [ 18 | .product(name: "MediaPicker", package: "swiftui-mediapicker"), 19 | ]), 20 | ``` 21 | 22 | 4. Add `import MediaPicker` to your source code. 23 | 24 | ## Example 25 | 26 | ```swift 27 | import SwiftUI 28 | import AVKit 29 | import MediaPicker 30 | 31 | struct ContentView: View { 32 | @State var urls: [URL] = [] 33 | @State var isShowingMediaPicker = false 34 | 35 | var selectButton: some View { 36 | Button("Select Media") { 37 | isShowingMediaPicker = true 38 | } 39 | .mediaImporter(isPresented: $isShowingMediaPicker, 40 | allowedMediaTypes: .all, 41 | allowsMultipleSelection: true) { result in 42 | switch result { 43 | case .success(let urls): 44 | self.urls = urls 45 | case .failure(let error): 46 | print(error) 47 | self.urls = [] 48 | } 49 | } 50 | } 51 | 52 | var body: some View { 53 | List { 54 | Section { 55 | ForEach(urls, id: \.absoluteString) { url in 56 | switch try! url.resourceValues(forKeys: [.contentTypeKey]).contentType! { 57 | case let contentType where contentType.conforms(to: .image): 58 | AsyncImage(url: url) { image in 59 | image 60 | .resizable() 61 | .scaledToFit() 62 | } placeholder: { 63 | ProgressView() 64 | } 65 | case let contentType where contentType.conforms(to: .audiovisualContent): 66 | VideoPlayer(player: AVPlayer(url: url)) 67 | .scaledToFit() 68 | default: 69 | Text("Can't display \(url.lastPathComponent)") 70 | } 71 | } 72 | } header: { 73 | selectButton 74 | } 75 | } 76 | } 77 | } 78 | ``` 79 | -------------------------------------------------------------------------------- /Sources/MediaPicker/MediaOptions.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftUI FoodTracker tutorial series 4 | // 5 | // Copyright (c) 2020-2022 AppDev@UW.edu and the SwiftUI MediaPicker authors 6 | // Licensed under MIT License 7 | // 8 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/blob/main/LICENSE 9 | // for license information 10 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/graphs/contributors 11 | // for the list of SwiftUI MediaPicker project authors 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | @_implementationOnly import UniformTypeIdentifiers 16 | 17 | public struct MediaTypeOptions: OptionSet { 18 | public let rawValue: Int 19 | 20 | public init(rawValue: Int) { 21 | self.rawValue = rawValue 22 | } 23 | 24 | /// Only Live Photos. 25 | public static let livePhotos = MediaTypeOptions(rawValue: 1 << 0) 26 | /// Images, including Live Photos. 27 | public static let images: MediaTypeOptions = [MediaTypeOptions(rawValue: 1 << 1), livePhotos] 28 | /// Only videos. 29 | public static let videos = MediaTypeOptions(rawValue: 1 << 2) 30 | 31 | /// All media types. 32 | public static let all: MediaTypeOptions = [.images, .videos] 33 | 34 | /// All uniform type identifiers for contained media types. 35 | internal var typeIdentifiers: [UTType] { 36 | var types = [UTType]() 37 | if contains(.images) { 38 | types += [.heic, .heif, .image] 39 | } else if contains(.livePhotos) { 40 | types += [.livePhoto, .heic] 41 | } 42 | if contains(.videos) { 43 | types.append(.audiovisualContent) 44 | } 45 | return types 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/MediaPicker/MediaPicker-PhotosUI.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftUI FoodTracker tutorial series 4 | // 5 | // Copyright (c) 2020-2022 AppDev@UW.edu and the SwiftUI MediaPicker authors 6 | // Licensed under MIT License 7 | // 8 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/blob/main/LICENSE 9 | // for license information 10 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/graphs/contributors 11 | // for the list of SwiftUI MediaPicker project authors 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | #if canImport(PhotosUI) && os(iOS) 16 | import SwiftUI 17 | import PhotosUI 18 | 19 | public extension View { 20 | /// - Important: You are responsible for setting `isPresented` to false. 21 | func mediaImporter( 22 | isPresented: Binding, 23 | allowedMediaTypes: MediaTypeOptions, 24 | allowsMultipleSelection: Bool, 25 | onCompletion: @escaping (Result<[PHPickerResult], Error>) -> Void, 26 | @ViewBuilder loadingOverlay: @escaping () -> LoadingOverlay 27 | ) -> some View { 28 | var configuration = PHPickerConfiguration(photoLibrary: .shared()) 29 | configuration.selectionLimit = allowsMultipleSelection ? 0 : 1 30 | configuration.filter = PHPickerFilter.from(allowedMediaTypes) 31 | 32 | return sheet(isPresented: isPresented) { 33 | MediaPickerWrapper( 34 | isPresented: isPresented, 35 | allowedContentTypes: allowedMediaTypes.typeIdentifiers, 36 | configuration: configuration, 37 | onCompletion: onCompletion, 38 | makeLoadingOverlay: loadingOverlay 39 | ) 40 | } 41 | } 42 | } 43 | 44 | fileprivate struct MediaPickerWrapper: View { 45 | @Binding var isPresented: Bool 46 | @State var isLoading: Bool = false 47 | let allowedContentTypes: [UTType] 48 | let configuration: PHPickerConfiguration 49 | let onCompletion: (Result<[PHPickerResult], Error>) -> Void 50 | let makeLoadingOverlay: () -> LoadingOverlay 51 | 52 | var body: some View { 53 | MediaPicker( 54 | isPresented: $isPresented, 55 | isLoading: $isLoading, 56 | allowedContentTypes: allowedContentTypes, 57 | configuration: configuration, 58 | onCompletion: onCompletion 59 | ) 60 | .overlay(isLoading ? makeLoadingOverlay() : nil) 61 | } 62 | } 63 | 64 | fileprivate extension PHPickerFilter { 65 | static func from(_ mediaOptions: MediaTypeOptions) -> Self { 66 | var filters = [PHPickerFilter]() 67 | if mediaOptions.contains(.images) { 68 | filters.append(.images) 69 | } else if mediaOptions.contains(.livePhotos) { 70 | filters.append(.livePhotos) 71 | } 72 | if mediaOptions.contains(.videos) { 73 | filters.append(.videos) 74 | } 75 | return PHPickerFilter.any(of: filters) 76 | } 77 | } 78 | 79 | // Meet the new Photos picker 80 | // https://developer.apple.com/wwdc20/10652 81 | fileprivate struct MediaPicker: UIViewControllerRepresentable { 82 | @Binding var isPresented: Bool 83 | @Binding var isLoading: Bool 84 | let allowedContentTypes: [UTType] 85 | let configuration: PHPickerConfiguration 86 | let onCompletion: (Result<[PHPickerResult], Error>) -> Void 87 | 88 | func makeUIViewController(context: Context) -> PHPickerViewController { 89 | let controller = PHPickerViewController(configuration: configuration) 90 | controller.delegate = context.coordinator 91 | return controller 92 | } 93 | 94 | func updateUIViewController(_ uiViewController: PHPickerViewController, 95 | context: Context) { 96 | // do nothing 97 | } 98 | 99 | func makeCoordinator() -> Coordinator { 100 | return Coordinator(for: self) 101 | } 102 | 103 | class Coordinator: PHPickerViewControllerDelegate { 104 | let coordinated: MediaPicker 105 | 106 | init(for picker: MediaPicker) { 107 | self.coordinated = picker 108 | } 109 | 110 | func picker(_ picker: PHPickerViewController, 111 | didFinishPicking results: [PHPickerResult]) { 112 | guard !results.isEmpty else { 113 | coordinated.isPresented = false 114 | return 115 | } 116 | Task { @MainActor in 117 | withAnimation { 118 | coordinated.isLoading = true 119 | } 120 | } 121 | coordinated.onCompletion(.success(results)) 122 | } 123 | } 124 | } 125 | #endif 126 | -------------------------------------------------------------------------------- /Sources/MediaPicker/MediaPicker-iOS.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftUI FoodTracker tutorial series 4 | // 5 | // Copyright (c) 2020-2022 AppDev@UW.edu and the SwiftUI MediaPicker authors 6 | // Licensed under MIT License 7 | // 8 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/blob/main/LICENSE 9 | // for license information 10 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/graphs/contributors 11 | // for the list of SwiftUI MediaPicker project authors 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | #if os(iOS) 16 | import SwiftUI 17 | import struct PhotosUI.PHPickerResult 18 | @_implementationOnly import UniformTypeIdentifiers 19 | @_implementationOnly import struct AVFoundation.AVError 20 | @_implementationOnly import os 21 | 22 | private func OSLog(category: String) -> os.OSLog { 23 | #if DEBUG 24 | return OSLog(subsystem: "dev.uwapp.MediaPicker", category: category) 25 | #else 26 | return .disabled 27 | #endif 28 | } 29 | 30 | public extension View { 31 | /// Presents a system interface for allowing the user to import an existing 32 | /// media. 33 | /// 34 | /// In order for the interface to appear, `isPresented` must be `true`. When 35 | /// the operation is finished, `isPresented` will be set to `false` before 36 | /// `onCompletion` is called. If the user cancels the operation, 37 | /// `isPresented` will be set to `false` and `onCompletion` will not be 38 | /// called. 39 | /// 40 | /// - Note: Changing `allowedMediaTypes` while the media importer is 41 | /// presented will have no immediate effect, however will apply the next 42 | /// time it is presented. 43 | /// 44 | /// - Parameters: 45 | /// - isPresented: A binding to whether the interface should be shown. 46 | /// - allowedMediaTypes: The list of supported media types which can 47 | /// be imported. 48 | /// - onCompletion: A callback that will be invoked when the operation has 49 | /// succeeded or failed. 50 | /// - result: A `Result` indicating whether the operation succeeded or 51 | /// failed. 52 | func mediaImporter( 53 | isPresented: Binding, 54 | allowedMediaTypes: MediaTypeOptions, 55 | onCompletion: @escaping (Result) -> Void 56 | ) -> some View { 57 | self.mediaImporter( 58 | isPresented: isPresented, 59 | allowedMediaTypes: allowedMediaTypes, 60 | onCompletion: onCompletion, 61 | loadingOverlay: DefaultLoadingOverlay.init 62 | ) 63 | } 64 | 65 | func mediaImporter( 66 | isPresented: Binding, 67 | allowedMediaTypes: MediaTypeOptions, 68 | onCompletion: @escaping (Result) -> Void, 69 | @ViewBuilder loadingOverlay: @escaping (Progress) -> LoadingOverlay 70 | ) -> some View { 71 | self.mediaImporter( 72 | isPresented: isPresented, 73 | allowedMediaTypes: allowedMediaTypes, 74 | allowsMultipleSelection: false, 75 | onCompletion: { result in 76 | onCompletion(result.map { $0.first! }) 77 | }, 78 | loadingOverlay: loadingOverlay 79 | ) 80 | } 81 | 82 | /// Presents a system interface for allowing the user to import multiple 83 | /// medium. 84 | /// 85 | /// In order for the interface to appear, `isPresented` must be `true`. When 86 | /// the operation is finished, `isPresented` will be set to `false` before 87 | /// `onCompletion` is called. If the user cancels the operation, 88 | /// `isPresented` will be set to `false` and `onCompletion` will not be 89 | /// called. 90 | /// 91 | /// - Note: Changing `allowedMediaTypes` or `allowsMultipleSelection` 92 | /// while the media importer is presented will have no immediate effect, 93 | /// however will apply the next time it is presented. 94 | /// 95 | /// - Parameters: 96 | /// - isPresented: A binding to whether the interface should be shown. 97 | /// - allowedMediaTypes: The list of supported media types which can 98 | /// be imported. 99 | /// - allowsMultipleSelection: Whether the importer allows the user to 100 | /// select more than one media to import. 101 | /// - onCompletion: A callback that will be invoked when the operation has 102 | /// succeeded or failed. 103 | /// - result: A `Result` indicating whether the operation succeeded or 104 | /// failed. 105 | func mediaImporter( 106 | isPresented: Binding, 107 | allowedMediaTypes: MediaTypeOptions, 108 | allowsMultipleSelection: Bool, 109 | onCompletion: @escaping (Result<[URL], Error>) -> Void 110 | ) -> some View { 111 | self.mediaImporter( 112 | isPresented: isPresented, 113 | allowedMediaTypes: allowedMediaTypes, 114 | allowsMultipleSelection: allowsMultipleSelection, 115 | onCompletion: onCompletion, 116 | loadingOverlay: DefaultLoadingOverlay.init 117 | ) 118 | } 119 | 120 | func mediaImporter( 121 | isPresented: Binding, 122 | allowedMediaTypes: MediaTypeOptions, 123 | allowsMultipleSelection: Bool, 124 | onCompletion: @escaping (Result<[URL], Error>) -> Void, 125 | @ViewBuilder loadingOverlay: @escaping (Progress) -> LoadingOverlay 126 | ) -> some View { 127 | let progress = Progress() 128 | return self.mediaImporter( 129 | isPresented: isPresented, 130 | allowedMediaTypes: allowedMediaTypes, 131 | allowsMultipleSelection: allowsMultipleSelection 132 | ) { (result: Result<[PHPickerResult], Error>) in 133 | switch result { 134 | case .success(let phPickerResults): 135 | importAsURLs(phPickerResults, 136 | allowedMediaTypes: allowedMediaTypes, 137 | progress: progress) { result in 138 | isPresented.wrappedValue = false 139 | onCompletion(result) 140 | } 141 | case .failure(let error): 142 | isPresented.wrappedValue = false 143 | onCompletion(.failure(error)) 144 | } 145 | } loadingOverlay: { 146 | loadingOverlay(progress) 147 | } 148 | } 149 | } 150 | 151 | fileprivate struct DefaultLoadingOverlay: View { 152 | let progress: Progress 153 | var body: some View { 154 | NavigationView { 155 | ProgressView(progress) 156 | .padding() 157 | .navigationTitle("Importing Media...") 158 | } 159 | .transition(.move(edge: .bottom).combined(with: .opacity)) 160 | } 161 | } 162 | 163 | fileprivate func importAsURLs(_ phPickerResults: [PHPickerResult], 164 | allowedMediaTypes: MediaTypeOptions, 165 | progress: Progress, 166 | onCompletion: @escaping (Result<[URL], Error>) -> Void) { 167 | let log = OSLog(category: "imageURLs") 168 | let signpostID = OSSignpostID(log: log, object: phPickerResults as NSArray) 169 | os_signpost(.begin, log: log, name: "imageURLs task group", signpostID: signpostID, 170 | "Loading %d results", phPickerResults.count) 171 | 172 | var imageURLs = [URL?](repeating: nil, count: phPickerResults.count) 173 | var errors = [Error]() 174 | var finishedCount = 0 175 | let queue = DispatchQueue(label: UUID().uuidString) 176 | progress.totalUnitCount = Int64(phPickerResults.count) 177 | progress.completedUnitCount = 0 178 | 179 | func recordResult(_ result: Result, for index: Int) { 180 | queue.sync { 181 | finishedCount += 1 182 | 183 | switch result { 184 | case .success(let url): 185 | os_signpost(.event, log: log, name: "imageURLs add url", signpostID: signpostID, 186 | "Adding %d out of %d results, url: %{public}@", 187 | finishedCount, phPickerResults.count, url.path) 188 | imageURLs[index] = url 189 | case .failure(let error): 190 | os_signpost(.event, log: log, name: "imageURLs add url", signpostID: signpostID, 191 | "Adding %d out of %d results, error: %{public}@", 192 | finishedCount, phPickerResults.count, error.localizedDescription) 193 | errors.append(error) 194 | } 195 | 196 | guard finishedCount == phPickerResults.count else { 197 | return 198 | } 199 | 200 | let resultImageURLs = imageURLs.compactMap { $0 } 201 | if errors.isEmpty { 202 | os_signpost(.end, log: log, name: "imageURLs task group", signpostID: signpostID, 203 | "success") 204 | onCompletion(.success(resultImageURLs)) 205 | } else { 206 | os_signpost(.end, log: log, name: "imageURLs task group", signpostID: signpostID, 207 | "errored: %d", errors.count) 208 | onCompletion(.failure(MediaPickerErrors.imageURL(resultImageURLs, errors: errors))) 209 | } 210 | } 211 | } 212 | 213 | os_signpost(.begin, log: log, name: "imageURLs add task", signpostID: signpostID, 214 | "Adding %d tasks", phPickerResults.count) 215 | pickerResultsLoop: 216 | for (index, result) in phPickerResults.enumerated() { 217 | let provider = result.itemProvider 218 | // TOOD: investigate should we instead use/consider 219 | // provider.registeredTypeIdentifiers 220 | for type in allowedMediaTypes.typeIdentifiers { 221 | if provider.hasItemConformingToTypeIdentifier(type.identifier) { 222 | os_signpost(.event, log: log, name: "imageURLs add task", signpostID: signpostID, 223 | "Adding %d out of %d tasks: '%{public}@' of type %{public}@", 224 | index + 1, phPickerResults.count, provider.suggestedName ?? "", type.identifier) 225 | 226 | let signpostID = OSSignpostID(log: log, object: provider) 227 | os_signpost(.begin, log: log, name: "fileURL loadFileRepresentation", signpostID: signpostID, 228 | "%{public}@", provider.suggestedName ?? "") 229 | // https://developer.apple.com/forums/thread/652496 230 | let loadingProgress = provider.loadFileRepresentation(forTypeIdentifier: type.identifier) { url, error in 231 | guard let src = url else { 232 | os_signpost(.end, log: log, name: "fileURL loadFileRepresentation", signpostID: signpostID, 233 | "errored, no src url") 234 | return recordResult(.failure(error!), for: index) 235 | } 236 | os_signpost(.end, log: log, name: "fileURL loadFileRepresentation", signpostID: signpostID) 237 | 238 | // Because the src/url will be deleted once we return, 239 | // will copy the stored image to a different temp url. 240 | let dst = FileManager.default.temporaryDirectory 241 | .appendingPathComponent(src.lastPathComponent) 242 | os_signpost(.begin, log: log, name: "fileURL copy", signpostID: signpostID, 243 | "fileURL copy from %@ to %@", src.path, dst.path) 244 | if FileManager.default.fileExists(atPath: dst.path) { 245 | os_signpost(.end, log: log, name: "fileURL copy", signpostID: signpostID, 246 | "already exists") 247 | return recordResult(.success(dst), for: index) 248 | } else { 249 | do { 250 | try FileManager.default.copyItem(at: src, to: dst) 251 | os_signpost(.end, log: log, name: "fileURL copy", signpostID: signpostID, 252 | "copied") 253 | return recordResult(.success(dst), for: index) 254 | } catch { 255 | os_signpost(.end, log: log, name: "fileURL copy", signpostID: signpostID, 256 | "errored: %{public}d", error.localizedDescription) 257 | return recordResult(.failure(error), for: index) 258 | } 259 | } 260 | } 261 | progress.addChild(loadingProgress, withPendingUnitCount: 1) 262 | continue pickerResultsLoop 263 | } 264 | } 265 | os_signpost(.event, log: log, name: "imageURLs add task", signpostID: signpostID, 266 | "Adding %d out of %d tasks: '%{public}@' can't be loaded, only has %{public}@", 267 | index + 1, phPickerResults.count, provider.suggestedName ?? "", provider.registeredTypeIdentifiers) 268 | recordResult(.failure(MediaPickerErrors.missingFileRepresentation), for: index) 269 | } 270 | os_signpost(.end, log: log, name: "imageURLs add task", signpostID: signpostID) 271 | } 272 | #endif 273 | -------------------------------------------------------------------------------- /Sources/MediaPicker/MediaPicker-macOS.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftUI FoodTracker tutorial series 4 | // 5 | // Copyright (c) 2020-2022 AppDev@UW.edu and the SwiftUI MediaPicker authors 6 | // Licensed under MIT License 7 | // 8 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/blob/main/LICENSE 9 | // for license information 10 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/graphs/contributors 11 | // for the list of SwiftUI MediaPicker project authors 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | #if os(macOS) 16 | import SwiftUI 17 | 18 | public extension View { 19 | /// Presents a system interface for allowing the user to import an existing 20 | /// media. 21 | /// 22 | /// In order for the interface to appear, `isPresented` must be `true`. When 23 | /// the operation is finished, `isPresented` will be set to `false` before 24 | /// `onCompletion` is called. If the user cancels the operation, 25 | /// `isPresented` will be set to `false` and `onCompletion` will not be 26 | /// called. 27 | /// 28 | /// - Note: Changing `allowedMediaTypes` while the media importer is 29 | /// presented will have no immediate effect, however will apply the next 30 | /// time it is presented. 31 | /// 32 | /// - Parameters: 33 | /// - isPresented: A binding to whether the interface should be shown. 34 | /// - allowedMediaTypes: The list of supported media types which can 35 | /// be imported. 36 | /// - onCompletion: A callback that will be invoked when the operation has 37 | /// succeeded or failed. 38 | /// - result: A `Result` indicating whether the operation succeeded or 39 | /// failed. 40 | func mediaImporter( 41 | isPresented: Binding, 42 | allowedMediaTypes: MediaTypeOptions, 43 | onCompletion: @escaping (Result) -> Void 44 | ) -> some View { 45 | self.fileImporter(isPresented: isPresented, 46 | allowedContentTypes: allowedMediaTypes.typeIdentifiers, 47 | onCompletion: onCompletion) 48 | } 49 | 50 | /// Presents a system interface for allowing the user to import multiple 51 | /// medium. 52 | /// 53 | /// In order for the interface to appear, `isPresented` must be `true`. When 54 | /// the operation is finished, `isPresented` will be set to `false` before 55 | /// `onCompletion` is called. If the user cancels the operation, 56 | /// `isPresented` will be set to `false` and `onCompletion` will not be 57 | /// called. 58 | /// 59 | /// - Note: Changing `allowedMediaTypes` or `allowsMultipleSelection` 60 | /// while the media importer is presented will have no immediate effect, 61 | /// however will apply the next time it is presented. 62 | /// 63 | /// - Parameters: 64 | /// - isPresented: A binding to whether the interface should be shown. 65 | /// - allowedMediaTypes: The list of supported media types which can 66 | /// be imported. 67 | /// - allowsMultipleSelection: Whether the importer allows the user to 68 | /// select more than one media to import. 69 | /// - onCompletion: A callback that will be invoked when the operation has 70 | /// succeeded or failed. 71 | /// - result: A `Result` indicating whether the operation succeeded or 72 | /// failed. 73 | func mediaImporter( 74 | isPresented: Binding, 75 | allowedMediaTypes: MediaTypeOptions, 76 | allowsMultipleSelection: Bool, 77 | onCompletion: @escaping (Result<[URL], Error>) -> Void 78 | ) -> some View { 79 | self.fileImporter(isPresented: isPresented, 80 | allowedContentTypes: allowedMediaTypes.typeIdentifiers, 81 | allowsMultipleSelection: allowsMultipleSelection, 82 | onCompletion: onCompletion) 83 | } 84 | } 85 | #endif 86 | -------------------------------------------------------------------------------- /Sources/MediaPicker/MediaPickerErrors.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftUI FoodTracker tutorial series 4 | // 5 | // Copyright (c) 2020-2022 AppDev@UW.edu and the SwiftUI MediaPicker authors 6 | // Licensed under MIT License 7 | // 8 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/blob/main/LICENSE 9 | // for license information 10 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/graphs/contributors 11 | // for the list of SwiftUI MediaPicker project authors 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import Foundation 16 | 17 | public enum MediaPickerErrors: Error { 18 | case imageURL([URL], errors: [Error]) 19 | case missingFileRepresentation 20 | } 21 | -------------------------------------------------------------------------------- /Tests/MediaPickerTests/MediaPickerTests.swift: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the SwiftUI FoodTracker tutorial series 4 | // 5 | // Copyright (c) 2020-2022 AppDev@UW.edu and the SwiftUI MediaPicker authors 6 | // Licensed under MIT License 7 | // 8 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/blob/main/LICENSE 9 | // for license information 10 | // See https://github.com/UWAppDev/SwiftUI-MediaPicker/graphs/contributors 11 | // for the list of SwiftUI MediaPicker project authors 12 | // 13 | //===----------------------------------------------------------------------===// 14 | 15 | import XCTest 16 | @testable import MediaPicker 17 | 18 | final class MediaPickerTests: XCTestCase { 19 | } 20 | --------------------------------------------------------------------------------