├── .github └── ISSUE_TEMPLATE │ └── config.yml ├── .gitignore ├── .travis.yml ├── BSImagePicker.podspec ├── BSImagePicker.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ ├── IDETemplateMacros.plist │ └── xcschemes │ └── BSImagePicker.xcscheme ├── BSImagePicker.xcworkspace ├── .xcodesamplecode.plist ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Example.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ └── Example.xcscheme ├── Example ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── 1024.png │ │ ├── 120-1.png │ │ ├── 120.png │ │ ├── 152.png │ │ ├── 167.png │ │ ├── 180.png │ │ ├── 20.png │ │ ├── 29.png │ │ ├── 40-1.png │ │ ├── 40-2.png │ │ ├── 40.png │ │ ├── 58-1.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 76.png │ │ ├── 80-1.png │ │ ├── 80.png │ │ ├── 87.png │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Cartfile ├── Info.plist ├── Podfile └── ViewController.swift ├── Info.plist ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── BSImagePicker.h ├── Controller │ ├── ImagePickerController+Albums.swift │ ├── ImagePickerController+Assets.swift │ ├── ImagePickerController+ButtonActions.swift │ ├── ImagePickerController+Closure.swift │ ├── ImagePickerController+PresentationDelegate.swift │ ├── ImagePickerController.swift │ └── ImagePickerControllerDelegate.swift ├── Extension │ └── UIColor+BSImagePicker.swift ├── Model │ ├── AssetStore.swift │ └── Settings.swift ├── Presentation │ ├── Dropdown │ │ ├── DropdownAnimator.swift │ │ ├── DropdownPresentationController.swift │ │ └── DropdownTransitionDelegate.swift │ └── Zoom │ │ ├── ZoomAnimator.swift │ │ ├── ZoomInteractionController.swift │ │ └── ZoomTransitionDelegate.swift ├── Resources │ └── PrivacyInfo.xcprivacy ├── Scene │ ├── Albums │ │ ├── AlbumCell.swift │ │ ├── AlbumsTableViewDataSource.swift │ │ └── AlbumsViewController.swift │ ├── Assets │ │ ├── AssetCollectionViewCell.swift │ │ ├── AssetsCollectionViewDataSource.swift │ │ ├── AssetsViewController.swift │ │ ├── CameraCollectionViewCell.swift │ │ ├── CheckmarkView.swift │ │ ├── GradientView.swift │ │ ├── NumberView.swift │ │ ├── SelectionView.swift │ │ └── VideoCollectionViewCell.swift │ ├── Camera │ │ ├── CameraPreviewView.swift │ │ └── CameraViewController.swift │ └── Preview │ │ ├── LivePreviewViewController.swift │ │ ├── PlayerView.swift │ │ ├── PreviewBuilder.swift │ │ ├── PreviewTitleBuilder.swift │ │ ├── PreviewViewController.swift │ │ └── VideoPreviewViewController.swift └── View │ ├── ArrowView.swift │ ├── CGSize+Scale.swift │ ├── ImageView.swift │ └── ImageViewLayout.swift ├── Tests ├── CGSizeExtensionTests.swift ├── CameraDataSourceTests.swift ├── ImagePickerViewTests.swift ├── Info.plist └── SettingsTests.swift └── UITests ├── Info.plist └── UITests.swift /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | 3 | b0rk 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Xcode 5 | build/ 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | profile 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | # Bundler 23 | .bundle 24 | 25 | # We recommend against adding the Pods directory to your .gitignore. However 26 | # you should judge for yourself, the pros and cons are mentioned at: 27 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control 28 | # 29 | # Note: if you ignore the Pods directory, make sure to uncomment 30 | # `pod install` in .travis.yml 31 | # 32 | # Pods/ 33 | 34 | Example/Podfile.lock 35 | Example/Pods/ 36 | 37 | # Carthage 38 | Carthage/Build 39 | 40 | .build/ 41 | .swiftpm/ 42 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # references: 2 | # * http://www.objc.io/issue-6/travis-ci.html 3 | # * https://github.com/supermarin/xcpretty#usage 4 | 5 | osx_image: xcode11 6 | language: objective-c 7 | #cache: cocoapods 8 | #podfile: Example/Podfile 9 | before_install: 10 | # - gem install cocoapods --no-rdoc --no-ri --no-document # Since Travis is not always on latest version 11 | # - pod update --project-directory=Example 12 | install: 13 | - gem install xcpretty --no-document --quiet 14 | script: 15 | - set -o pipefail && xcodebuild test -project BSImagePicker.xcodeproj -scheme BSImagePicker -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO -destination 'platform=iOS Simulator,name=iPhone 8,OS=13.0' | xcpretty -c 16 | #- pod lib lint --quick 17 | -------------------------------------------------------------------------------- /BSImagePicker.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "BSImagePicker" 3 | s.version = "3.3.3" 4 | s.summary = "BSImagePicker is a multiple image picker for iOS. UIImagePickerController replacement" 5 | s.description = <<-DESC 6 | A multiple image picker. 7 | It is intended as a replacement for UIImagePickerController for both selecting photos. 8 | DESC 9 | s.homepage = "https://github.com/mikaoj/BSImagePicker" 10 | s.license = 'MIT' 11 | s.author = { "Joakim Gyllström" => "joakim@backslashed.se" } 12 | s.source = { :git => "https://github.com/mikaoj/BSImagePicker.git", :tag => s.version.to_s } 13 | 14 | s.platform = :ios, '10.0' 15 | s.requires_arc = true 16 | s.swift_version = '5.7' 17 | 18 | s.source_files = 'Sources/**/*.swift' 19 | s.resource_bundle = { 'BSImagePicker' => ['Sources/Resources/PrivacyInfo.xcprivacy']} 20 | s.frameworks = 'UIKit', 'Photos' 21 | end 22 | -------------------------------------------------------------------------------- /BSImagePicker.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /BSImagePicker.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /BSImagePicker.xcodeproj/xcshareddata/IDETemplateMacros.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FILEHEADER 6 | The MIT License (MIT) 7 | // 8 | // Copyright (c) ___YEAR___ ___FULLUSERNAME___ 9 | // 10 | // Permission is hereby granted, free of charge, to any person obtaining a copy 11 | // of this software and associated documentation files (the "Software"), to deal 12 | // in the Software without restriction, including without limitation the rights 13 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | // copies of the Software, and to permit persons to whom the Software is 15 | // furnished to do so, subject to the following conditions: 16 | // 17 | // The above copyright notice and this permission notice shall be included in all 18 | // copies or substantial portions of the Software. 19 | // 20 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | // SOFTWARE. 27 | 28 | 29 | -------------------------------------------------------------------------------- /BSImagePicker.xcodeproj/xcshareddata/xcschemes/BSImagePicker.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /BSImagePicker.xcworkspace/.xcodesamplecode.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/BSImagePicker.xcworkspace/.xcodesamplecode.plist -------------------------------------------------------------------------------- /BSImagePicker.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /BSImagePicker.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 64 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Joakim Gyllström on 2019-03-05. 6 | // Copyright © 2019 Joakim Gyllström. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/120-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/120-1.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/40-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/40-1.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/40-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/40-2.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/58-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/58-1.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/80-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/80-1.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikaoj/BSImagePicker/87c300fac63538dfb26e17d491eaa2293e83b5e8/Example/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /Example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "40.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "60.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "58.png", 19 | "scale" : "2x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "87.png", 25 | "scale" : "3x" 26 | }, 27 | { 28 | "size" : "40x40", 29 | "idiom" : "iphone", 30 | "filename" : "80.png", 31 | "scale" : "2x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "120.png", 37 | "scale" : "3x" 38 | }, 39 | { 40 | "size" : "60x60", 41 | "idiom" : "iphone", 42 | "filename" : "120-1.png", 43 | "scale" : "2x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "180.png", 49 | "scale" : "3x" 50 | }, 51 | { 52 | "size" : "20x20", 53 | "idiom" : "ipad", 54 | "filename" : "20.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "40-1.png", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "size" : "29x29", 65 | "idiom" : "ipad", 66 | "filename" : "29.png", 67 | "scale" : "1x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "58-1.png", 73 | "scale" : "2x" 74 | }, 75 | { 76 | "size" : "40x40", 77 | "idiom" : "ipad", 78 | "filename" : "40-2.png", 79 | "scale" : "1x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "80-1.png", 85 | "scale" : "2x" 86 | }, 87 | { 88 | "size" : "76x76", 89 | "idiom" : "ipad", 90 | "filename" : "76.png", 91 | "scale" : "1x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "152.png", 97 | "scale" : "2x" 98 | }, 99 | { 100 | "size" : "83.5x83.5", 101 | "idiom" : "ipad", 102 | "filename" : "167.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "1024x1024", 107 | "idiom" : "ios-marketing", 108 | "filename" : "1024.png", 109 | "scale" : "1x" 110 | } 111 | ], 112 | "info" : { 113 | "version" : 1, 114 | "author" : "xcode" 115 | } 116 | } -------------------------------------------------------------------------------- /Example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Example/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 31 | 44 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Example/Cartfile: -------------------------------------------------------------------------------- 1 | git "file:///Users/mikaoj/Code/Backslashed/BSImagePicker" "develop" 2 | -------------------------------------------------------------------------------- /Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | NSCameraUsageDescription 24 | 25 | NSPhotoLibraryUsageDescription 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Example/Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | 3 | platform :ios, '10.0' 4 | inhibit_all_warnings! 5 | workspace '../BSImagePicker' 6 | 7 | 8 | target 'Example' do 9 | project '../Example' 10 | pod 'BSImagePicker', :path => '../' 11 | end 12 | -------------------------------------------------------------------------------- /Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Joakim Gyllström 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 | 23 | import UIKit 24 | import BSImagePicker 25 | import Photos 26 | 27 | class ViewController: UIViewController { 28 | 29 | @IBAction func showImagePicker(_ sender: UIButton) { 30 | let imagePicker = ImagePickerController() 31 | imagePicker.settings.selection.max = 5 32 | imagePicker.settings.theme.selectionStyle = .numbered 33 | imagePicker.settings.fetch.assets.supportedMediaTypes = [.image, .video] 34 | imagePicker.settings.selection.unselectOnReachingMax = true 35 | 36 | let start = Date() 37 | self.presentImagePicker(imagePicker, select: { (asset) in 38 | print("Selected: \(asset)") 39 | }, deselect: { (asset) in 40 | print("Deselected: \(asset)") 41 | }, cancel: { (assets) in 42 | print("Canceled with selections: \(assets)") 43 | }, finish: { (assets) in 44 | print("Finished with selections: \(assets)") 45 | }, completion: { 46 | let finish = Date() 47 | print(finish.timeIntervalSince(start)) 48 | }) 49 | } 50 | 51 | @IBAction func showCustomImagePicker(_ sender: UIButton) { 52 | let imagePicker = ImagePickerController() 53 | imagePicker.settings.selection.max = 1 54 | imagePicker.settings.selection.unselectOnReachingMax = true 55 | imagePicker.settings.fetch.assets.supportedMediaTypes = [.image, .video] 56 | imagePicker.albumButton.tintColor = UIColor.green 57 | imagePicker.cancelButton.tintColor = UIColor.red 58 | imagePicker.doneButton.tintColor = UIColor.purple 59 | imagePicker.navigationBar.barTintColor = .black 60 | imagePicker.settings.theme.backgroundColor = .black 61 | imagePicker.settings.theme.selectionFillColor = UIColor.gray 62 | imagePicker.settings.theme.selectionStrokeColor = UIColor.yellow 63 | imagePicker.settings.theme.selectionShadowColor = UIColor.red 64 | imagePicker.settings.theme.previewTitleAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16),NSAttributedString.Key.foregroundColor: UIColor.white] 65 | imagePicker.settings.theme.previewSubtitleAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12),NSAttributedString.Key.foregroundColor: UIColor.white] 66 | imagePicker.settings.theme.albumTitleAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18),NSAttributedString.Key.foregroundColor: UIColor.white] 67 | imagePicker.settings.list.cellsPerRow = {(verticalSize: UIUserInterfaceSizeClass, horizontalSize: UIUserInterfaceSizeClass) -> Int in 68 | switch (verticalSize, horizontalSize) { 69 | case (.compact, .regular): // iPhone5-6 portrait 70 | return 2 71 | case (.compact, .compact): // iPhone5-6 landscape 72 | return 2 73 | case (.regular, .regular): // iPad portrait/landscape 74 | return 3 75 | default: 76 | return 2 77 | } 78 | } 79 | 80 | self.presentImagePicker(imagePicker, select: { (asset) in 81 | print("Selected: \(asset)") 82 | }, deselect: { (asset) in 83 | print("Deselected: \(asset)") 84 | }, cancel: { (assets) in 85 | print("Canceled with selections: \(assets)") 86 | }, finish: { (assets) in 87 | print("Finished with selections: \(assets)") 88 | }) 89 | } 90 | 91 | @IBAction func showImagePickerWithSelectedAssets(_ sender: UIButton) { 92 | let allAssets = PHAsset.fetchAssets(with: PHAssetMediaType.image, options: nil) 93 | var evenAssets = [PHAsset]() 94 | 95 | allAssets.enumerateObjects({ (asset, idx, stop) -> Void in 96 | if idx % 2 == 0 { 97 | evenAssets.append(asset) 98 | } 99 | }) 100 | 101 | let imagePicker = ImagePickerController(selectedAssets: evenAssets) 102 | imagePicker.settings.fetch.assets.supportedMediaTypes = [.image] 103 | 104 | self.presentImagePicker(imagePicker, select: { (asset) in 105 | print("Selected: \(asset)") 106 | }, deselect: { (asset) in 107 | print("Deselected: \(asset)") 108 | }, cancel: { (assets) in 109 | print("Canceled with selections: \(assets)") 110 | }, finish: { (assets) in 111 | print("Finished with selections: \(assets)") 112 | }) 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | $(MARKETING_VERSION) 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Joakim Gyllstrom 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.7 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: "BSImagePicker", 8 | platforms: [.iOS(.v12)], 9 | products: [ 10 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 11 | .library( 12 | name: "BSImagePicker", 13 | targets: ["BSImagePicker"]), 14 | ], 15 | dependencies: [ 16 | // Dependencies declare other packages that this package depends on. 17 | // .package(url: /* package url */, from: "1.0.0"), 18 | ], 19 | targets: [ 20 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 21 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 22 | .target( 23 | name: "BSImagePicker", 24 | dependencies: [], 25 | path: "Sources", 26 | resources: [ 27 | .copy("Resources/PrivacyInfo.xcprivacy") 28 | ] 29 | ), 30 | .testTarget( 31 | name: "Tests", 32 | dependencies: ["BSImagePicker"], 33 | path: "Tests"), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BSImagePicker 2 | [![CI Status](http://img.shields.io/travis/mikaoj/BSImagePicker.svg?style=flat)](https://travis-ci.org/mikaoj/BSImagePicker) 3 | [![Version](https://img.shields.io/cocoapods/v/BSImagePicker.svg?style=flat)](http://cocoapods.org/pods/BSImagePicker) 4 | [![License](https://img.shields.io/cocoapods/l/BSImagePicker.svg?style=flat)](http://cocoapods.org/pods/BSImagePicker) 5 | [![Platform](https://img.shields.io/cocoapods/p/BSImagePicker.svg?style=flat)](http://cocoapods.org/pods/BSImagePicker) 6 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 7 | 8 | ![alt text](https://cloud.githubusercontent.com/assets/4034956/15001931/254805de-119c-11e6-9f68-d815ccc712cd.gif "Demo gif") 9 | 10 | A multiple image picker for iOS. 11 | 12 | ## Features 13 | * Multiple selection. 14 | * Fullscreen preview 15 | * Switching albums. 16 | * Supports images, live photos and videos. 17 | * Customizable. 18 | 19 | ## Usage 20 | 21 | ##### Info.plist 22 | To be able to request permission to the users photo library you need to add this to your Info.plist 23 | ``` 24 | NSPhotoLibraryUsageDescription 25 | Why you want to access photo library 26 | ``` 27 | 28 | ##### Image picker 29 | ``` 30 | import BSImagePicker 31 | 32 | let imagePicker = ImagePickerController() 33 | 34 | presentImagePicker(imagePicker, select: { (asset) in 35 | // User selected an asset. Do something with it. Perhaps begin processing/upload? 36 | }, deselect: { (asset) in 37 | // User deselected an asset. Cancel whatever you did when asset was selected. 38 | }, cancel: { (assets) in 39 | // User canceled selection. 40 | }, finish: { (assets) in 41 | // User finished selection assets. 42 | }) 43 | ``` 44 | 45 | ##### PHAsset 46 | So you have a bunch of [PHAsset](https://developer.apple.com/documentation/photokit/phasset)s now, great. But how do you use them? 47 | To get an UIImage from the asset you use a [PHImageManager](https://developer.apple.com/documentation/photokit/phimagemanager). 48 | 49 | ``` 50 | import Photos 51 | 52 | // Request the maximum size. If you only need a smaller size make sure to request that instead. 53 | PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .aspectFit, options: nil) { (image, info) in 54 | // Do something with image 55 | } 56 | ``` 57 | 58 | For more example you can clone this repo and look at the example app. 59 | 60 | ## Installation 61 | 62 | ### Cocoapods 63 | Add the following line to your Podfile: 64 | 65 | ``` 66 | pod "BSImagePicker", "~> 3.1" 67 | ``` 68 | ### Carthage 69 | Add the following line to your Cartfile: 70 | ``` 71 | github "mikaoj/BSImagePicker" ~> 3.1 72 | ``` 73 | ### Swift Package Manager 74 | Add it to the dependencies value of your Package.swift.: 75 | ``` 76 | dependencies: [ 77 | .package(url: "https://github.com/mikaoj/BSImagePicker.git", from: "version-tag") 78 | ] 79 | ``` 80 | 81 | ## Xamarin 82 | 83 | If you are Xamarin developer you can use [Net.Xamarin.iOS.BSImagePicker](https://github.com/SByteDev/Net.Xamarin.iOS.BSImagePicker) 84 | 85 | ## Contribution 86 | 87 | Users are encouraged to become active participants in its continued development — by fixing any bugs that they encounter, or by improving the documentation wherever it’s found to be lacking. 88 | 89 | If you wish to make a change, [open a Pull Request](https://github.com/mikaoj/BSImagePicker/pull/new) — even if it just contains a draft of the changes you’re planning, or a test that reproduces an issue — and we can discuss it further from there. 90 | 91 | ## License 92 | 93 | BSImagePicker is available under the MIT license. See the LICENSE file for more info. 94 | -------------------------------------------------------------------------------- /Sources/BSImagePicker.h: -------------------------------------------------------------------------------- 1 | // 2 | // BSImagePicker.h 3 | // BSImagePicker 4 | // 5 | // Created by Joakim Gyllström on 2018-12-27. 6 | // Copyright © 2018 Joakim Gyllström. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for BSImagePicker. 12 | FOUNDATION_EXPORT double BSImagePickerVersionNumber; 13 | 14 | //! Project version string for BSImagePicker. 15 | FOUNDATION_EXPORT const unsigned char BSImagePickerVersionString[]; 16 | 17 | // In this header, you should import all the public headers of your framework using statements like #import 18 | 19 | 20 | -------------------------------------------------------------------------------- /Sources/Controller/ImagePickerController+Albums.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import Foundation 24 | import Photos 25 | 26 | extension ImagePickerController: AlbumsViewControllerDelegate { 27 | func didDismissAlbumsViewController(_ albumsViewController: AlbumsViewController) { 28 | rotateButtonArrow() 29 | } 30 | 31 | func albumsViewController(_ albumsViewController: AlbumsViewController, didSelectAlbum album: PHAssetCollection) { 32 | select(album: album) 33 | albumsViewController.dismiss(animated: true) 34 | } 35 | 36 | func select(album: PHAssetCollection) { 37 | assetsViewController.showAssets(in: album) 38 | albumButton.setTitle((album.localizedTitle ?? "") + " ", for: .normal) 39 | albumButton.sizeToFit() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Controller/ImagePickerController+Assets.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import Foundation 24 | import Photos 25 | 26 | extension ImagePickerController: AssetsViewControllerDelegate { 27 | func assetsViewController(_ assetsViewController: AssetsViewController, didSelectAsset asset: PHAsset) { 28 | if settings.selection.unselectOnReachingMax && assetStore.count > settings.selection.max { 29 | if let first = assetStore.removeFirst() { 30 | assetsViewController.unselect(asset:first) 31 | imagePickerDelegate?.imagePicker(self, didDeselectAsset: first) 32 | } 33 | } 34 | updatedDoneButton() 35 | imagePickerDelegate?.imagePicker(self, didSelectAsset: asset) 36 | 37 | if assetStore.count >= settings.selection.max { 38 | imagePickerDelegate?.imagePicker(self, didReachSelectionLimit: assetStore.count) 39 | } 40 | } 41 | 42 | func assetsViewController(_ assetsViewController: AssetsViewController, didDeselectAsset asset: PHAsset) { 43 | updatedDoneButton() 44 | imagePickerDelegate?.imagePicker(self, didDeselectAsset: asset) 45 | } 46 | 47 | func assetsViewController(_ assetsViewController: AssetsViewController, didLongPressCell cell: AssetCollectionViewCell, displayingAsset asset: PHAsset) { 48 | let previewViewController = PreviewBuilder.createPreviewController(for: asset, with: settings) 49 | 50 | zoomTransitionDelegate.zoomedOutView = cell.imageView 51 | zoomTransitionDelegate.zoomedInView = previewViewController.imageView 52 | 53 | pushViewController(previewViewController, animated: true) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/Controller/ImagePickerController+ButtonActions.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import UIKit 24 | 25 | extension ImagePickerController { 26 | @objc func albumsButtonPressed(_ sender: UIButton) { 27 | albumsViewController.albums = albums 28 | 29 | // Setup presentation controller 30 | albumsViewController.transitioningDelegate = dropdownTransitionDelegate 31 | albumsViewController.modalPresentationStyle = .custom 32 | rotateButtonArrow() 33 | 34 | present(albumsViewController, animated: true) 35 | } 36 | 37 | @objc func doneButtonPressed(_ sender: UIBarButtonItem) { 38 | imagePickerDelegate?.imagePicker(self, didFinishWithAssets: assetStore.assets) 39 | 40 | if settings.dismiss.enabled { 41 | dismiss(animated: true) 42 | } 43 | } 44 | 45 | @objc func cancelButtonPressed(_ sender: UIBarButtonItem) { 46 | imagePickerDelegate?.imagePicker(self, didCancelWithAssets: assetStore.assets) 47 | 48 | if settings.dismiss.enabled { 49 | dismiss(animated: true) 50 | } 51 | } 52 | 53 | func rotateButtonArrow() { 54 | UIView.animate(withDuration: 0.3) { [weak self] in 55 | guard let imageView = self?.albumButton.imageView else { return } 56 | imageView.transform = imageView.transform.rotated(by: .pi) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Controller/ImagePickerController+Closure.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import UIKit 24 | import Photos 25 | 26 | /// Closure convenience API. 27 | /// Keep this simple enough for most users. More niche features can be added to ImagePickerControllerDelegate. 28 | @objc extension UIViewController { 29 | 30 | /// Present a image picker 31 | /// 32 | /// - Parameters: 33 | /// - imagePicker: The image picker to present 34 | /// - animated: Should presentation be animated 35 | /// - select: Selection callback 36 | /// - deselect: Deselection callback 37 | /// - cancel: Cancel callback 38 | /// - finish: Finish callback 39 | /// - completion: Presentation completion callback 40 | public func presentImagePicker(_ imagePicker: ImagePickerController, animated: Bool = true, select: ((_ asset: PHAsset) -> Void)?, deselect: ((_ asset: PHAsset) -> Void)?, cancel: (([PHAsset]) -> Void)?, finish: (([PHAsset]) -> Void)?, completion: (() -> Void)? = nil) { 41 | authorize { 42 | // Set closures 43 | imagePicker.onSelection = select 44 | imagePicker.onDeselection = deselect 45 | imagePicker.onCancel = cancel 46 | imagePicker.onFinish = finish 47 | 48 | // And since we are using the blocks api. Set ourselfs as delegate 49 | imagePicker.imagePickerDelegate = imagePicker 50 | 51 | // Present 52 | self.present(imagePicker, animated: animated, completion: completion) 53 | } 54 | } 55 | 56 | private func authorize(_ authorized: @escaping () -> Void) { 57 | PHPhotoLibrary.requestAuthorization { (status) in 58 | switch status { 59 | case .authorized: 60 | DispatchQueue.main.async(execute: authorized) 61 | default: 62 | break 63 | } 64 | } 65 | } 66 | } 67 | 68 | extension ImagePickerController { 69 | public static var currentAuthorization : PHAuthorizationStatus { 70 | return PHPhotoLibrary.authorizationStatus() 71 | } 72 | } 73 | 74 | /// ImagePickerControllerDelegate closure wrapper 75 | extension ImagePickerController: ImagePickerControllerDelegate { 76 | public func imagePicker(_ imagePicker: ImagePickerController, didSelectAsset asset: PHAsset) { 77 | onSelection?(asset) 78 | } 79 | 80 | public func imagePicker(_ imagePicker: ImagePickerController, didDeselectAsset asset: PHAsset) { 81 | onDeselection?(asset) 82 | } 83 | 84 | public func imagePicker(_ imagePicker: ImagePickerController, didFinishWithAssets assets: [PHAsset]) { 85 | onFinish?(assets) 86 | } 87 | 88 | public func imagePicker(_ imagePicker: ImagePickerController, didCancelWithAssets assets: [PHAsset]) { 89 | onCancel?(assets) 90 | } 91 | 92 | public func imagePicker(_ imagePicker: ImagePickerController, didReachSelectionLimit count: Int) { 93 | 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Sources/Controller/ImagePickerController+PresentationDelegate.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2020 Felix Lisczyk 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 | 23 | import UIKit 24 | 25 | extension ImagePickerController: UIAdaptivePresentationControllerDelegate { 26 | 27 | @available(iOS 13, *) 28 | public func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { 29 | return settings.dismiss.enabled && settings.dismiss.allowSwipe 30 | } 31 | 32 | // This method is only called if 33 | // - the presented view controller is not dismissed programmatically and 34 | // - its `isModalInPresentation` property is set to false. 35 | @available(iOS 13, *) 36 | public func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { 37 | imagePickerDelegate?.imagePicker(self, didCancelWithAssets: assetStore.assets) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Controller/ImagePickerController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import UIKit 24 | import Photos 25 | 26 | // MARK: ImagePickerController 27 | @objc(BSImagePickerController) 28 | @objcMembers open class ImagePickerController: UINavigationController { 29 | // MARK: Public properties 30 | public weak var imagePickerDelegate: ImagePickerControllerDelegate? 31 | public var settings: Settings = Settings() 32 | public var doneButton: UIBarButtonItem = UIBarButtonItem(title: "", style: .done, target: nil, action: nil) 33 | public var cancelButton: UIBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: nil, action: nil) 34 | public var albumButton: UIButton = UIButton(type: .custom) 35 | public var selectedAssets: [PHAsset] { 36 | get { 37 | return assetStore.assets 38 | } 39 | } 40 | 41 | /// Title to use for button 42 | public var doneButtonTitle = Bundle(for: UIBarButtonItem.self).localizedString(forKey: "Done", value: "Done", table: "") 43 | 44 | // MARK: Internal properties 45 | var assetStore: AssetStore 46 | var onSelection: ((_ asset: PHAsset) -> Void)? 47 | var onDeselection: ((_ asset: PHAsset) -> Void)? 48 | var onCancel: ((_ assets: [PHAsset]) -> Void)? 49 | var onFinish: ((_ assets: [PHAsset]) -> Void)? 50 | 51 | let assetsViewController: AssetsViewController 52 | let albumsViewController = AlbumsViewController() 53 | let dropdownTransitionDelegate = DropdownTransitionDelegate() 54 | let zoomTransitionDelegate = ZoomTransitionDelegate() 55 | 56 | lazy var albums: [PHAssetCollection] = { 57 | // We don't want collections without assets. 58 | // I would like to do that with PHFetchOptions: fetchOptions.predicate = NSPredicate(format: "estimatedAssetCount > 0") 59 | // But that doesn't work... 60 | // This seems suuuuuper ineffective... 61 | let fetchOptions = settings.fetch.assets.options.copy() as! PHFetchOptions 62 | fetchOptions.fetchLimit = 1 63 | 64 | return settings.fetch.album.fetchResults.filter { 65 | $0.count > 0 66 | }.flatMap { 67 | $0.objects(at: IndexSet(integersIn: 0..<$0.count)) 68 | }.filter { 69 | // We can't use estimatedAssetCount on the collection 70 | // It returns NSNotFound. So actually fetch the assets... 71 | let assetsFetchResult = PHAsset.fetchAssets(in: $0, options: fetchOptions) 72 | return assetsFetchResult.count > 0 73 | } 74 | }() 75 | 76 | public init(selectedAssets: [PHAsset] = []) { 77 | assetStore = AssetStore(assets: selectedAssets) 78 | assetsViewController = AssetsViewController(store: assetStore) 79 | super.init(nibName: nil, bundle: nil) 80 | } 81 | 82 | public required init?(coder aDecoder: NSCoder) { 83 | fatalError("init(coder:) has not been implemented") 84 | } 85 | 86 | public override func viewDidLoad() { 87 | super.viewDidLoad() 88 | 89 | // Sync settings 90 | albumsViewController.settings = settings 91 | assetsViewController.settings = settings 92 | 93 | // Setup view controllers 94 | albumsViewController.delegate = self 95 | assetsViewController.delegate = self 96 | 97 | viewControllers = [assetsViewController] 98 | view.backgroundColor = settings.theme.backgroundColor 99 | 100 | // Setup delegates 101 | delegate = zoomTransitionDelegate 102 | presentationController?.delegate = self 103 | 104 | // Turn off translucency so drop down can match its color 105 | navigationBar.isTranslucent = false 106 | navigationBar.isOpaque = true 107 | 108 | // Setup buttons 109 | let firstViewController = viewControllers.first 110 | albumButton.setTitleColor(albumButton.tintColor, for: .normal) 111 | albumButton.titleLabel?.font = .systemFont(ofSize: 16) 112 | albumButton.titleLabel?.adjustsFontSizeToFitWidth = true 113 | 114 | let arrowView = ArrowView(frame: CGRect(x: 0, y: 0, width: 8, height: 8)) 115 | arrowView.backgroundColor = .clear 116 | arrowView.strokeColor = albumButton.tintColor 117 | let image = arrowView.asImage 118 | 119 | albumButton.setImage(image, for: .normal) 120 | albumButton.semanticContentAttribute = .forceRightToLeft // To set image to the right without having to calculate insets/constraints. 121 | albumButton.addTarget(self, action: #selector(ImagePickerController.albumsButtonPressed(_:)), for: .touchUpInside) 122 | firstViewController?.navigationItem.titleView = albumButton 123 | 124 | doneButton.target = self 125 | doneButton.action = #selector(doneButtonPressed(_:)) 126 | firstViewController?.navigationItem.rightBarButtonItem = doneButton 127 | 128 | cancelButton.target = self 129 | cancelButton.action = #selector(cancelButtonPressed(_:)) 130 | firstViewController?.navigationItem.leftBarButtonItem = cancelButton 131 | 132 | updatedDoneButton() 133 | updateAlbumButton() 134 | 135 | // We need to have some color to be able to match with the drop down 136 | if navigationBar.barTintColor == nil { 137 | navigationBar.barTintColor = .systemBackgroundColor 138 | } 139 | 140 | if let firstAlbum = albums.first { 141 | select(album: firstAlbum) 142 | } 143 | } 144 | 145 | public func deselect(asset: PHAsset) { 146 | assetStore.remove(asset) 147 | assetsViewController.unselect(asset: asset) 148 | updatedDoneButton() 149 | } 150 | 151 | func updatedDoneButton() { 152 | doneButton.title = assetStore.count > 0 ? doneButtonTitle + " (\(assetStore.count))" : doneButtonTitle 153 | 154 | doneButton.isEnabled = assetStore.count >= settings.selection.min 155 | } 156 | 157 | func updateAlbumButton() { 158 | albumButton.isHidden = albums.count < 2 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Sources/Controller/ImagePickerControllerDelegate.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import Foundation 24 | import Photos 25 | 26 | /// Delegate of the image picker 27 | public protocol ImagePickerControllerDelegate: class { 28 | /// An asset was selected 29 | /// - Parameter imagePicker: The image picker that asset was selected in 30 | /// - Parameter asset: selected asset 31 | func imagePicker(_ imagePicker: ImagePickerController, didSelectAsset asset: PHAsset) 32 | 33 | /// An asset was deselected 34 | /// - Parameter imagePicker: The image picker that asset was deselected in 35 | /// - Parameter asset: deselected asset 36 | func imagePicker(_ imagePicker: ImagePickerController, didDeselectAsset asset: PHAsset) 37 | 38 | /// User finished with selecting assets 39 | /// - Parameter imagePicker: The image picker that assets where selected in 40 | /// - Parameter assets: Selected assets 41 | func imagePicker(_ imagePicker: ImagePickerController, didFinishWithAssets assets: [PHAsset]) 42 | 43 | /// User canceled selecting assets 44 | /// - Parameter imagePicker: The image picker that asset was selected in 45 | /// - Parameter assets: Assets selected when user canceled 46 | func imagePicker(_ imagePicker: ImagePickerController, didCancelWithAssets assets: [PHAsset]) 47 | 48 | /// Selection limit reach 49 | /// - Parameter imagePicker: The image picker that selection limit was reached in. 50 | /// - Parameter count: Number of selected assets. 51 | func imagePicker(_ imagePicker: ImagePickerController, didReachSelectionLimit count: Int) 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Extension/UIColor+BSImagePicker.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2020 Shashank Mishra 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 | 23 | import UIKit 24 | 25 | extension UIColor { 26 | 27 | static var systemBackgroundColor: UIColor { 28 | if #available(iOS 13.0, *) { 29 | return systemBackground 30 | } else { 31 | // Same old color used for iOS 12 and earlier 32 | return .white 33 | } 34 | } 35 | 36 | static var systemShadowColor: UIColor { 37 | if #available(iOS 13.0, *) { 38 | return tertiarySystemBackground 39 | } else { 40 | // Same old color used for iOS 12 and earlier 41 | return .black 42 | } 43 | } 44 | 45 | static var systemPrimaryTextColor: UIColor { 46 | if #available(iOS 13.0, *) { 47 | return label 48 | } else { 49 | // Same old color used for iOS 12 and earlier 50 | return .black 51 | } 52 | } 53 | 54 | static var systemSecondaryTextColor: UIColor { 55 | if #available(iOS 13.0, *) { 56 | return secondaryLabel 57 | } else { 58 | // Same old color used for iOS 12 and earlier 59 | return .black 60 | } 61 | } 62 | 63 | static var systemStrokeColor: UIColor { 64 | if #available(iOS 13.0, *) { 65 | return UIColor { (traitCollection: UITraitCollection) -> UIColor in 66 | if traitCollection.userInterfaceStyle == .dark { 67 | return white 68 | } 69 | else { 70 | return black 71 | }} 72 | } else { 73 | // Same old color used for iOS 12 and earlier 74 | return .black 75 | } 76 | } 77 | 78 | static var systemOverlayColor: UIColor { 79 | if #available(iOS 13.0, *) { 80 | return secondarySystemBackground 81 | } else { 82 | // Same old color used for iOS 12 and earlier 83 | return .lightGray 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/Model/AssetStore.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import Foundation 24 | import Photos 25 | 26 | @objcMembers public class AssetStore : NSObject { 27 | public private(set) var assets: [PHAsset] 28 | 29 | public init(assets: [PHAsset] = []) { 30 | self.assets = assets 31 | } 32 | 33 | public var count: Int { 34 | return assets.count 35 | } 36 | 37 | func contains(_ asset: PHAsset) -> Bool { 38 | return assets.contains(asset) 39 | } 40 | 41 | func append(_ asset: PHAsset) { 42 | guard contains(asset) == false else { return } 43 | assets.append(asset) 44 | } 45 | 46 | func remove(_ asset: PHAsset) { 47 | guard let index = assets.firstIndex(of: asset) else { return } 48 | assets.remove(at: index) 49 | } 50 | 51 | func removeFirst() -> PHAsset? { 52 | return assets.removeFirst() 53 | } 54 | 55 | func index(of asset: PHAsset) -> Int? { 56 | return assets.firstIndex(of: asset) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Model/Settings.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Joakim Gyllström 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 | 23 | import UIKit 24 | import Photos 25 | 26 | @objc(BSImagePickerSettings) // Fix for ObjC header name conflicting. 27 | @objcMembers public class Settings : NSObject { 28 | public static let shared = Settings() 29 | 30 | // Move all theme related stuff to UIAppearance 31 | public class Theme : NSObject { 32 | /// Main background color 33 | public lazy var backgroundColor: UIColor = .systemBackgroundColor 34 | 35 | /// Color for backgroun of drop downs 36 | public lazy var dropDownBackgroundColor: UIColor = .clear 37 | 38 | /// What color to fill the circle with 39 | public lazy var selectionFillColor: UIColor = UIView().tintColor 40 | 41 | /// Color for the actual checkmark 42 | public lazy var selectionStrokeColor: UIColor = .white 43 | 44 | /// Shadow color for the circle 45 | public lazy var selectionShadowColor: UIColor = .systemShadowColor 46 | 47 | public enum SelectionStyle { 48 | case checked 49 | case numbered 50 | } 51 | 52 | /// The icon to display inside the selection oval 53 | public lazy var selectionStyle: SelectionStyle = .checked 54 | 55 | public lazy var previewTitleAttributes : [NSAttributedString.Key: Any] = [ 56 | NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16), 57 | NSAttributedString.Key.foregroundColor: UIColor.systemPrimaryTextColor 58 | ] 59 | 60 | public lazy var previewSubtitleAttributes: [NSAttributedString.Key: Any] = [ 61 | NSAttributedString.Key.font: UIFont.systemFont(ofSize: 12), 62 | NSAttributedString.Key.foregroundColor: UIColor.systemSecondaryTextColor 63 | ] 64 | 65 | public lazy var albumTitleAttributes: [NSAttributedString.Key: Any] = [ 66 | NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18), 67 | NSAttributedString.Key.foregroundColor: UIColor.systemPrimaryTextColor 68 | ] 69 | } 70 | 71 | @objc(BSImagePickerSelection) 72 | @objcMembers public class Selection : NSObject { 73 | /// Max number of selections allowed 74 | public lazy var max: Int = Int.max 75 | 76 | /// Min number of selections you have to make 77 | public lazy var min: Int = 1 78 | 79 | /// If it reaches the max limit, unselect the first selection, and allow the new selection 80 | @objc public lazy var unselectOnReachingMax : Bool = false 81 | } 82 | 83 | @objc(BSImagePickerList) 84 | @objcMembers public class List : NSObject { 85 | /// How much spacing between cells 86 | public lazy var spacing: CGFloat = 2 87 | 88 | /// How many cells per row 89 | public lazy var cellsPerRow: (_ verticalSize: UIUserInterfaceSizeClass, _ horizontalSize: UIUserInterfaceSizeClass) -> Int = {(verticalSize: UIUserInterfaceSizeClass, horizontalSize: UIUserInterfaceSizeClass) -> Int in 90 | switch (verticalSize, horizontalSize) { 91 | case (.compact, .regular): // iPhone5-6 portrait 92 | return 3 93 | case (.compact, .compact): // iPhone5-6 landscape 94 | return 5 95 | case (.regular, .regular): // iPad portrait/landscape 96 | return 7 97 | default: 98 | return 3 99 | } 100 | } 101 | } 102 | 103 | public class Preview : NSObject { 104 | /// Is preview enabled? 105 | public lazy var enabled: Bool = true 106 | } 107 | 108 | @objc(BSImagePickerFetch) 109 | @objcMembers public class Fetch : NSObject { 110 | @objc(BSImagePickerAlbum) 111 | @objcMembers public class Album : NSObject { 112 | /// Fetch options for albums/collections 113 | public lazy var options: PHFetchOptions = { 114 | let fetchOptions = PHFetchOptions() 115 | return fetchOptions 116 | }() 117 | 118 | /// Fetch results for asset collections you want to present to the user 119 | /// Some other fetch results that you might wanna use: 120 | /// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumFavorites, options: options), 121 | /// PHAssetCollection.fetchAssetCollections(with: .album, subtype: .albumRegular, options: options), 122 | /// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumSelfPortraits, options: options), 123 | /// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumPanoramas, options: options), 124 | /// PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumVideos, options: options), 125 | public lazy var fetchResults: [PHFetchResult] = [ 126 | PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: options), 127 | ] 128 | } 129 | 130 | @objc(BSImagePickerAssets) 131 | @objcMembers public class Assets : NSObject { 132 | /// Fetch options for assets 133 | 134 | /// Simple wrapper around PHAssetMediaType to ensure we only expose the supported types. 135 | public enum MediaTypes { 136 | case image 137 | case video 138 | 139 | fileprivate var assetMediaType: PHAssetMediaType { 140 | switch self { 141 | case .image: 142 | return .image 143 | case .video: 144 | return .video 145 | } 146 | } 147 | } 148 | public lazy var supportedMediaTypes: Set = [.image] 149 | 150 | public lazy var options: PHFetchOptions = { 151 | let fetchOptions = PHFetchOptions() 152 | fetchOptions.sortDescriptors = [ 153 | NSSortDescriptor(key: "creationDate", ascending: false) 154 | ] 155 | 156 | let rawMediaTypes = supportedMediaTypes.map { $0.assetMediaType.rawValue } 157 | let predicate = NSPredicate(format: "mediaType IN %@", rawMediaTypes) 158 | fetchOptions.predicate = predicate 159 | 160 | return fetchOptions 161 | }() 162 | } 163 | 164 | public class Preview : NSObject { 165 | public lazy var photoOptions: PHImageRequestOptions = { 166 | let options = PHImageRequestOptions() 167 | options.isNetworkAccessAllowed = true 168 | 169 | return options 170 | }() 171 | 172 | public lazy var livePhotoOptions: PHLivePhotoRequestOptions = { 173 | let options = PHLivePhotoRequestOptions() 174 | options.isNetworkAccessAllowed = true 175 | return options 176 | }() 177 | 178 | public lazy var videoOptions: PHVideoRequestOptions = { 179 | let options = PHVideoRequestOptions() 180 | options.isNetworkAccessAllowed = true 181 | return options 182 | }() 183 | } 184 | 185 | /// Album fetch settings 186 | public lazy var album = Album() 187 | 188 | /// Asset fetch settings 189 | public lazy var assets = Assets() 190 | 191 | /// Preview fetch settings 192 | public lazy var preview = Preview() 193 | } 194 | 195 | public class Dismiss : NSObject { 196 | /// Should the image picker dismiss when done/cancelled 197 | public lazy var enabled = true 198 | 199 | /// Allow the user to dismiss the image picker by swiping down 200 | public lazy var allowSwipe = false 201 | } 202 | 203 | /// Theme settings 204 | public lazy var theme = Theme() 205 | 206 | /// Selection settings 207 | public lazy var selection = Selection() 208 | 209 | /// List settings 210 | public lazy var list = List() 211 | 212 | /// Fetch settings 213 | public lazy var fetch = Fetch() 214 | 215 | /// Dismiss settings 216 | public lazy var dismiss = Dismiss() 217 | 218 | /// Preview options 219 | public lazy var preview = Preview() 220 | } 221 | -------------------------------------------------------------------------------- /Sources/Presentation/Dropdown/DropdownAnimator.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import Foundation 24 | import UIKit 25 | 26 | class DropdownAnimator: NSObject, UIViewControllerAnimatedTransitioning { 27 | enum Context { 28 | case present 29 | case dismiss 30 | } 31 | 32 | private let context: Context 33 | 34 | init(context: Context) { 35 | self.context = context 36 | super.init() 37 | } 38 | 39 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 40 | return 0.3 41 | } 42 | 43 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 44 | let isPresenting = context == .present 45 | let viewControllerKey: UITransitionContextViewControllerKey = isPresenting ? .to : .from 46 | 47 | guard let viewController = transitionContext.viewController(forKey: viewControllerKey) else { return } 48 | 49 | if isPresenting { 50 | transitionContext.containerView.addSubview(viewController.view) 51 | } 52 | 53 | let presentedFrame = transitionContext.finalFrame(for: viewController) 54 | let dismissedFrame = CGRect(x: presentedFrame.origin.x, y: presentedFrame.origin.y, width: presentedFrame.width, height: 0) 55 | 56 | let initialFrame = isPresenting ? dismissedFrame : presentedFrame 57 | let finalFrame = isPresenting ? presentedFrame : dismissedFrame 58 | 59 | let animationDuration = transitionDuration(using: transitionContext) 60 | viewController.view.frame = initialFrame 61 | UIView.animate(withDuration: animationDuration, animations: { 62 | viewController.view.frame = finalFrame 63 | }) { finished in 64 | transitionContext.completeTransition(finished) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Sources/Presentation/Dropdown/DropdownPresentationController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import Foundation 24 | import UIKit 25 | 26 | class DropdownPresentationController: UIPresentationController { 27 | private let dropDownHeight: CGFloat = 200 28 | private let backgroundView = UIView() 29 | 30 | override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) { 31 | super.init(presentedViewController: presentedViewController, presenting: presentingViewController) 32 | 33 | backgroundView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 34 | backgroundView.backgroundColor = .clear 35 | backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(backgroundTapped(_:)))) 36 | } 37 | 38 | @objc func backgroundTapped(_ recognizer: UIGestureRecognizer) { 39 | presentingViewController.dismiss(animated: true) 40 | } 41 | 42 | override func presentationTransitionWillBegin() { 43 | guard let containerView = containerView else { return } 44 | 45 | containerView.insertSubview(backgroundView, at: 0) 46 | backgroundView.frame = containerView.bounds 47 | } 48 | 49 | override func containerViewWillLayoutSubviews() { 50 | presentedView?.frame = frameOfPresentedViewInContainerView 51 | } 52 | 53 | override func size(forChildContentContainer container: UIContentContainer, withParentContainerSize parentSize: CGSize) -> CGSize { 54 | return CGSize(width: parentSize.width, height: dropDownHeight) 55 | } 56 | 57 | override var frameOfPresentedViewInContainerView: CGRect { 58 | guard let containerView = containerView, 59 | let presentingView = presentingViewController.view else { return .zero } 60 | 61 | let size = self.size(forChildContentContainer: presentedViewController, 62 | withParentContainerSize: presentingView.bounds.size) 63 | 64 | let position: CGPoint 65 | if let navigationBar = (presentingViewController as? UINavigationController)?.navigationBar { 66 | // We can't use the frame directly since iOS 13 new modal presentation style 67 | let navigationRect = navigationBar.convert(navigationBar.bounds, to: nil) 68 | let presentingRect = presentingView.convert(presentingView.frame, to: containerView) 69 | position = CGPoint(x: presentingRect.origin.x, y: navigationRect.maxY) 70 | 71 | // Match color with navigation bar 72 | presentedViewController.view.backgroundColor = navigationBar.barTintColor 73 | } else { 74 | if #available(iOS 11.0, *) { 75 | position = CGPoint(x: containerView.safeAreaInsets.left, y: containerView.safeAreaInsets.top) 76 | } else { 77 | position = .zero 78 | } 79 | } 80 | 81 | return CGRect(origin: position, size: size) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/Presentation/Dropdown/DropdownTransitionDelegate.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import Foundation 24 | import UIKit 25 | 26 | class DropdownTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate { 27 | func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { 28 | return DropdownPresentationController(presentedViewController: presented, presenting: presenting) 29 | } 30 | 31 | func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 32 | return DropdownAnimator(context: .present) 33 | } 34 | 35 | func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 36 | return DropdownAnimator(context: .dismiss) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Presentation/Zoom/ZoomAnimator.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Joakim Gyllström 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 | 23 | import UIKit 24 | 25 | final class ZoomAnimator : NSObject, UIViewControllerAnimatedTransitioning { 26 | enum Mode { 27 | case expand 28 | case shrink 29 | } 30 | 31 | var sourceImageView: UIImageView? 32 | var destinationImageView: UIImageView? 33 | let mode: Mode 34 | 35 | init(mode: Mode) { 36 | self.mode = mode 37 | super.init() 38 | } 39 | 40 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 41 | return 0.25 42 | } 43 | 44 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 45 | // Get to and from view controller 46 | if let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to), let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from), let sourceImageView = sourceImageView, let destinationImageView = destinationImageView { 47 | let containerView = transitionContext.containerView 48 | 49 | // Setup views 50 | sourceImageView.isHidden = true 51 | destinationImageView.isHidden = true 52 | containerView.backgroundColor = toViewController.view.backgroundColor 53 | 54 | // Setup scaling image 55 | let scalingFrame = containerView.convert(sourceImageView.frame, from: sourceImageView.superview) 56 | let scalingImage = ImageView(frame: scalingFrame) 57 | scalingImage.contentMode = sourceImageView.contentMode 58 | scalingImage.clipsToBounds = true 59 | 60 | if mode == .expand { 61 | toViewController.view.alpha = 0.0 62 | fromViewController.view.alpha = 1.0 63 | scalingImage.image = destinationImageView.image ?? sourceImageView.image 64 | } else { 65 | scalingImage.image = sourceImageView.image ?? destinationImageView.image 66 | } 67 | 68 | // Add views to container view 69 | containerView.addSubview(toViewController.view) 70 | containerView.addSubview(scalingImage) 71 | 72 | // Convert destination frame 73 | let destinationFrame = containerView.convert(destinationImageView.bounds, from: destinationImageView.superview) 74 | 75 | // Animate 76 | UIView.animate(withDuration: transitionDuration(using: transitionContext), 77 | delay: 0.0, 78 | options: [], 79 | animations: { () -> Void in 80 | // Fade in 81 | fromViewController.view.alpha = 0.0 82 | toViewController.view.alpha = 1.0 83 | 84 | scalingImage.frame = destinationFrame 85 | scalingImage.contentMode = destinationImageView.contentMode 86 | }, completion: { (finished) -> Void in 87 | scalingImage.removeFromSuperview() 88 | 89 | // Unhide 90 | destinationImageView.isHidden = false 91 | fromViewController.view.alpha = 1.0 92 | 93 | // Finish transition 94 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 95 | }) 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Sources/Presentation/Zoom/ZoomInteractionController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import UIKit 24 | 25 | class ZoomInteractionController: UIPercentDrivenInteractiveTransition, UIGestureRecognizerDelegate { 26 | private weak var navigationController: UINavigationController? 27 | private var containerView: UIView! 28 | private var sourceView: UIView! 29 | private var destinationView: UIView! 30 | private var hasCompleted = false 31 | private let separationView = UIView() 32 | private var transform: CGAffineTransform! 33 | private let threshold: CGFloat = 70 // How many pixels for swipe to dismiss 34 | 35 | override init() { 36 | super.init() 37 | wantsInteractiveStart = false 38 | } 39 | 40 | override func startInteractiveTransition(_ transitionContext: UIViewControllerContextTransitioning) { 41 | super.startInteractiveTransition(transitionContext) 42 | let viewController = transitionContext.viewController(forKey: .to) 43 | setupGestureRecognizer(in: viewController?.view) 44 | navigationController = viewController?.navigationController 45 | 46 | sourceView = transitionContext.view(forKey: .to) 47 | destinationView = transitionContext.view(forKey: .from) 48 | containerView = transitionContext.containerView 49 | transform = sourceView.transform 50 | } 51 | 52 | private func setupGestureRecognizer(in view: UIView?) { 53 | let gesture = UIPanGestureRecognizer(target: self, action: #selector(handlePan(_:))) 54 | gesture.delegate = self 55 | view?.addGestureRecognizer(gesture) 56 | } 57 | 58 | private var f: CGRect = .zero 59 | @objc func handlePan(_ panRecognizer: UIPanGestureRecognizer) { 60 | switch panRecognizer.state { 61 | case .began: 62 | begin() 63 | case .changed: 64 | let translation = panRecognizer.translation(in: panRecognizer.view!.superview!) 65 | update(translation: translation) 66 | case .cancelled: 67 | cancel() 68 | case .ended: 69 | if hasCompleted { 70 | finish() 71 | } else { 72 | cancel() 73 | } 74 | default: 75 | break 76 | } 77 | } 78 | 79 | func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 80 | guard let pan = gestureRecognizer as? UIPanGestureRecognizer else { return false } 81 | return translation(for: pan).y >= 0 82 | } 83 | 84 | private func translation(for pan: UIPanGestureRecognizer) -> CGPoint { 85 | return pan.translation(in: pan.view!.superview!) 86 | } 87 | 88 | private func percentDone(translation: CGPoint) -> CGFloat { 89 | guard translation.y > 0 else { return 0 } 90 | let progress = translation.y / threshold 91 | return min(progress, 1) 92 | } 93 | 94 | func begin() { 95 | containerView.addSubview(destinationView) 96 | containerView.addSubview(separationView) 97 | containerView.bringSubviewToFront(sourceView) 98 | separationView.backgroundColor = sourceView.backgroundColor 99 | separationView.frame = destinationView.frame 100 | sourceView.backgroundColor = sourceView.backgroundColor?.withAlphaComponent(0) 101 | transform = sourceView.transform 102 | } 103 | 104 | override func cancel() { 105 | super.cancel() 106 | sourceView.backgroundColor = sourceView.backgroundColor?.withAlphaComponent(1) 107 | sourceView.transform = transform 108 | destinationView.removeFromSuperview() 109 | separationView.removeFromSuperview() 110 | } 111 | 112 | override func finish() { 113 | super.finish() 114 | navigationController?.popViewController(animated: true) 115 | } 116 | 117 | func update(translation: CGPoint) { 118 | let progress = percentDone(translation: translation) 119 | super.update(progress) 120 | let y = translation.y > 0 ? translation.y : 0 121 | let scale = 1 - (y / destinationView.frame.size.height) 122 | let translateX = translation.x / scale 123 | let translateY = translation.y / scale 124 | sourceView.transform = CGAffineTransform.init(translationX: translateX, y: translateY).concatenating(CGAffineTransform.init(scaleX: scale, y: scale)) 125 | separationView.backgroundColor = separationView.backgroundColor?.withAlphaComponent(scale) 126 | hasCompleted = progress == 1 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /Sources/Presentation/Zoom/ZoomTransitionDelegate.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import UIKit 24 | 25 | class ZoomTransitionDelegate: NSObject, UIViewControllerTransitioningDelegate { 26 | var zoomedOutView: UIImageView? { 27 | didSet { 28 | expandAnimator.sourceImageView = zoomedOutView 29 | shrinkAnimator.destinationImageView = zoomedOutView 30 | } 31 | } 32 | var zoomedInView: UIImageView? { 33 | didSet { 34 | expandAnimator.destinationImageView = zoomedInView 35 | shrinkAnimator.sourceImageView = zoomedInView 36 | } 37 | } 38 | 39 | private let expandAnimator = ZoomAnimator(mode: .expand) 40 | private let shrinkAnimator = ZoomAnimator(mode: .shrink) 41 | private let interactionController = ZoomInteractionController() 42 | 43 | func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 44 | return expandAnimator 45 | } 46 | 47 | func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 48 | return shrinkAnimator 49 | } 50 | 51 | func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 52 | return ZoomInteractionController() 53 | } 54 | } 55 | 56 | extension ZoomTransitionDelegate: UINavigationControllerDelegate { 57 | public func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? { 58 | if operation == .push, toVC is PreviewViewController { 59 | return expandAnimator 60 | } else if operation == .pop, fromVC is PreviewViewController { 61 | return shrinkAnimator 62 | } else { 63 | return nil 64 | } 65 | } 66 | 67 | public func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 68 | if animationController === expandAnimator { 69 | return interactionController 70 | } else { 71 | return nil 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/Resources/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyCollectedDataTypes 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | 10 | NSPrivacyAccessedAPIType 11 | NSPrivacyAccessedAPICategoryFileTimestamp 12 | NSPrivacyAccessedAPITypeReasons 13 | 14 | C617.1 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Sources/Scene/Albums/AlbumCell.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Joakim Gyllström 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 | 23 | import UIKit 24 | 25 | /** 26 | Cell for photo albums in the albums drop down menu 27 | */ 28 | final class AlbumCell: UITableViewCell { 29 | static let identifier = "AlbumCell" 30 | 31 | let albumImageView: UIImageView = UIImageView(frame: .zero) 32 | let albumTitleLabel: UILabel = UILabel(frame: .zero) 33 | 34 | override var isSelected: Bool { 35 | didSet { 36 | // Selection checkmark 37 | if isSelected == true { 38 | accessoryType = .checkmark 39 | } else { 40 | accessoryType = .none 41 | } 42 | } 43 | } 44 | 45 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 46 | super.init(style: style, reuseIdentifier: reuseIdentifier) 47 | 48 | contentView.backgroundColor = UIColor.clear 49 | backgroundColor = UIColor.clear 50 | selectionStyle = .none 51 | 52 | albumImageView.translatesAutoresizingMaskIntoConstraints = false 53 | albumImageView.contentMode = .scaleAspectFill 54 | albumImageView.clipsToBounds = true 55 | contentView.addSubview(albumImageView) 56 | 57 | albumTitleLabel.translatesAutoresizingMaskIntoConstraints = false 58 | albumTitleLabel.numberOfLines = 0 59 | contentView.addSubview(albumTitleLabel) 60 | 61 | NSLayoutConstraint.activate([ 62 | albumImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8), 63 | albumImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -8), 64 | albumImageView.heightAnchor.constraint(equalToConstant: 84), 65 | albumImageView.widthAnchor.constraint(equalToConstant: 84), 66 | albumImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8), 67 | albumTitleLabel.leadingAnchor.constraint(equalTo: albumImageView.trailingAnchor, constant: 8), 68 | albumTitleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -8), 69 | albumTitleLabel.topAnchor.constraint(equalTo: contentView.topAnchor), 70 | albumTitleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 71 | ]) 72 | } 73 | 74 | required init?(coder aDecoder: NSCoder) { 75 | fatalError("init(coder:) has not been implemented") 76 | } 77 | 78 | override func prepareForReuse() { 79 | super.prepareForReuse() 80 | albumImageView.image = nil 81 | albumTitleLabel.text = nil 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Sources/Scene/Albums/AlbumsTableViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Joakim Gyllström 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 | 23 | import UIKit 24 | import Photos 25 | 26 | /** 27 | Implements the UITableViewDataSource protocol with a data source and cell factory 28 | */ 29 | final class AlbumsTableViewDataSource : NSObject, UITableViewDataSource { 30 | var settings: Settings! 31 | 32 | private let albums: [PHAssetCollection] 33 | private let scale: CGFloat 34 | private let imageManager = PHCachingImageManager.default() 35 | 36 | init(albums: [PHAssetCollection], scale: CGFloat = UIScreen.main.scale) { 37 | self.albums = albums 38 | self.scale = scale 39 | super.init() 40 | } 41 | 42 | func numberOfSections(in tableView: UITableView) -> Int { 43 | return albums.count > 0 ? 1 : 0 44 | } 45 | 46 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 47 | return albums.count 48 | } 49 | 50 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 51 | let cell = tableView.dequeueReusableCell(withIdentifier: AlbumCell.identifier, for: indexPath) as! AlbumCell 52 | 53 | // Fetch album 54 | let album = albums[indexPath.row] 55 | 56 | // Title 57 | cell.albumTitleLabel.attributedText = titleForAlbum(album) 58 | 59 | let fetchOptions = settings.fetch.assets.options.copy() as! PHFetchOptions 60 | fetchOptions.fetchLimit = 1 61 | 62 | let imageSize = CGSize(width: 84, height: 84).resize(by: scale) 63 | let imageContentMode: PHImageContentMode = .aspectFill 64 | if let asset = PHAsset.fetchAssets(in: album, options: fetchOptions).firstObject { 65 | imageManager.requestImage(for: asset, targetSize: imageSize, contentMode: imageContentMode, options: settings.fetch.preview.photoOptions) { (image, _) in 66 | guard let image = image else { return } 67 | cell.albumImageView.image = image 68 | } 69 | } 70 | 71 | return cell 72 | } 73 | 74 | func registerCells(in tableView: UITableView) { 75 | tableView.register(AlbumCell.self, forCellReuseIdentifier: AlbumCell.identifier) 76 | } 77 | 78 | private func titleForAlbum(_ album: PHAssetCollection) -> NSAttributedString { 79 | let text = NSMutableAttributedString() 80 | 81 | text.append(NSAttributedString(string: album.localizedTitle ?? "", attributes: settings.theme.albumTitleAttributes)) 82 | 83 | return text 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/Scene/Albums/AlbumsViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Joakim Gyllström 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 | 23 | import UIKit 24 | import Photos 25 | 26 | protocol AlbumsViewControllerDelegate: class { 27 | func albumsViewController(_ albumsViewController: AlbumsViewController, didSelectAlbum album: PHAssetCollection) 28 | func didDismissAlbumsViewController(_ albumsViewController: AlbumsViewController) 29 | } 30 | 31 | class AlbumsViewController: UIViewController { 32 | weak var delegate: AlbumsViewControllerDelegate? 33 | var settings: Settings! { 34 | didSet { dataSource?.settings = settings } 35 | } 36 | 37 | var albums: [PHAssetCollection] = [] 38 | private var dataSource: AlbumsTableViewDataSource? 39 | private let tableView: UITableView = UITableView(frame: .zero, style: .grouped) 40 | private let lineView: UIView = UIView() 41 | 42 | override func viewDidLoad() { 43 | super.viewDidLoad() 44 | 45 | dataSource = AlbumsTableViewDataSource(albums: albums) 46 | dataSource?.settings = settings 47 | 48 | tableView.frame = view.bounds 49 | tableView.autoresizingMask = [.flexibleHeight, .flexibleWidth] 50 | tableView.rowHeight = UITableView.automaticDimension 51 | tableView.estimatedRowHeight = 100 52 | tableView.separatorStyle = .none 53 | tableView.sectionHeaderHeight = .leastNormalMagnitude 54 | tableView.sectionFooterHeight = .leastNormalMagnitude 55 | tableView.showsVerticalScrollIndicator = false 56 | tableView.showsHorizontalScrollIndicator = false 57 | tableView.register(AlbumCell.self, forCellReuseIdentifier: AlbumCell.identifier) 58 | tableView.dataSource = dataSource 59 | tableView.delegate = self 60 | tableView.backgroundColor = settings.theme.dropDownBackgroundColor 61 | view.addSubview(tableView) 62 | 63 | let lineHeight: CGFloat = 0.5 64 | lineView.frame = view.bounds 65 | lineView.frame.size.height = lineHeight 66 | lineView.frame.origin.y = view.frame.size.height - lineHeight 67 | lineView.backgroundColor = .gray 68 | lineView.autoresizingMask = [.flexibleTopMargin, .flexibleWidth] 69 | view.addSubview(lineView) 70 | 71 | modalPresentationStyle = .popover 72 | preferredContentSize = CGSize(width: 320, height: 300) 73 | } 74 | 75 | override func viewWillDisappear(_ animated: Bool) { 76 | super.viewWillDisappear(animated) 77 | 78 | // Since AlbumsViewController is presented with a presentation controller 79 | // And we change the state of the album button depending on if it's presented or not 80 | // We need to get some sort of callback to update that state. 81 | // Perhaps do something else 82 | if isBeingDismissed { 83 | delegate?.didDismissAlbumsViewController(self) 84 | } 85 | } 86 | } 87 | 88 | extension AlbumsViewController: UITableViewDelegate { 89 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 90 | let album = albums[indexPath.row] 91 | delegate?.albumsViewController(self, didSelectAlbum: album) 92 | } 93 | 94 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 95 | return nil 96 | } 97 | 98 | func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { 99 | return nil 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Sources/Scene/Assets/AssetCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Joakim Gyllström 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 | 23 | import UIKit 24 | import Photos 25 | 26 | /** 27 | The photo cell. 28 | */ 29 | class AssetCollectionViewCell: UICollectionViewCell { 30 | let imageView: UIImageView = UIImageView(frame: .zero) 31 | var settings: Settings! { 32 | didSet { selectionView.settings = settings } 33 | } 34 | var selectionIndex: Int? { 35 | didSet { selectionView.selectionIndex = selectionIndex } 36 | } 37 | 38 | override var isSelected: Bool { 39 | didSet { 40 | guard oldValue != isSelected else { return } 41 | 42 | updateAccessibilityLabel(isSelected) 43 | if UIView.areAnimationsEnabled { 44 | UIView.animate(withDuration: TimeInterval(0.1), animations: { () -> Void in 45 | // Set alpha for views 46 | self.updateAlpha(self.isSelected) 47 | 48 | // Scale all views down a little 49 | self.transform = CGAffineTransform(scaleX: 0.95, y: 0.95) 50 | }, completion: { (finished: Bool) -> Void in 51 | UIView.animate(withDuration: TimeInterval(0.1), animations: { () -> Void in 52 | // And then scale them back upp again to give a bounce effect 53 | self.transform = CGAffineTransform(scaleX: 1.0, y: 1.0) 54 | }, completion: nil) 55 | }) 56 | } else { 57 | updateAlpha(isSelected) 58 | } 59 | } 60 | } 61 | 62 | private let selectionOverlayView: UIView = UIView(frame: .zero) 63 | private let selectionView: SelectionView = SelectionView(frame: .zero) 64 | 65 | override init(frame: CGRect) { 66 | super.init(frame: frame) 67 | 68 | // Setup views 69 | imageView.translatesAutoresizingMaskIntoConstraints = false 70 | imageView.contentMode = .scaleAspectFill 71 | imageView.clipsToBounds = true 72 | selectionOverlayView.backgroundColor = UIColor.systemOverlayColor 73 | selectionOverlayView.translatesAutoresizingMaskIntoConstraints = false 74 | selectionView.translatesAutoresizingMaskIntoConstraints = false 75 | contentView.addSubview(imageView) 76 | contentView.addSubview(selectionOverlayView) 77 | contentView.addSubview(selectionView) 78 | 79 | // Add constraints 80 | NSLayoutConstraint.activate([ 81 | imageView.topAnchor.constraint(equalTo: contentView.topAnchor), 82 | imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 83 | imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 84 | imageView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), 85 | selectionOverlayView.topAnchor.constraint(equalTo: contentView.topAnchor), 86 | selectionOverlayView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), 87 | selectionOverlayView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), 88 | selectionOverlayView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), 89 | selectionView.heightAnchor.constraint(equalToConstant: 25), 90 | selectionView.widthAnchor.constraint(equalToConstant: 25), 91 | selectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -4), 92 | selectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4) 93 | ]) 94 | 95 | updateAlpha(isSelected) 96 | updateAccessibilityLabel(isSelected) 97 | } 98 | 99 | required init?(coder aDecoder: NSCoder) { 100 | fatalError("init(coder:) has not been implemented") 101 | } 102 | 103 | override func prepareForReuse() { 104 | super.prepareForReuse() 105 | imageView.image = nil 106 | selectionIndex = nil 107 | } 108 | 109 | func updateAccessibilityLabel(_ selected: Bool) { 110 | accessibilityLabel = selected ? "deselect image" : "select image" 111 | } 112 | 113 | private func updateAlpha(_ selected: Bool) { 114 | if selected { 115 | self.selectionView.alpha = 1.0 116 | self.selectionOverlayView.alpha = 0.3 117 | } else { 118 | self.selectionView.alpha = 0.0 119 | self.selectionOverlayView.alpha = 0.0 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/Scene/Assets/AssetsCollectionViewDataSource.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Joakim Gyllström 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 | 23 | import UIKit 24 | import Photos 25 | 26 | class AssetsCollectionViewDataSource : NSObject, UICollectionViewDataSource { 27 | private static let assetCellIdentifier = "AssetCell" 28 | private static let videoCellIdentifier = "VideoCell" 29 | 30 | var settings: Settings! 31 | var fetchResult: PHFetchResult { 32 | didSet { 33 | imageManager.stopCachingImagesForAllAssets() 34 | } 35 | } 36 | var imageSize: CGSize = .zero { 37 | didSet { 38 | imageManager.stopCachingImagesForAllAssets() 39 | } 40 | } 41 | 42 | private let imageManager = PHCachingImageManager() 43 | private let durationFormatter = DateComponentsFormatter() 44 | private let store: AssetStore 45 | private let contentMode: PHImageContentMode = .aspectFill 46 | 47 | init(fetchResult: PHFetchResult, store: AssetStore) { 48 | self.fetchResult = fetchResult 49 | self.store = store 50 | durationFormatter.unitsStyle = .positional 51 | durationFormatter.zeroFormattingBehavior = [.pad] 52 | durationFormatter.allowedUnits = [.minute, .second] 53 | super.init() 54 | } 55 | 56 | func numberOfSections(in collectionView: UICollectionView) -> Int { 57 | return 1 58 | } 59 | 60 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 61 | return fetchResult.count 62 | } 63 | 64 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 65 | let asset = fetchResult[indexPath.row] 66 | let animationsWasEnabled = UIView.areAnimationsEnabled 67 | let cell: AssetCollectionViewCell 68 | 69 | UIView.setAnimationsEnabled(false) 70 | if asset.mediaType == .video { 71 | cell = collectionView.dequeueReusableCell(withReuseIdentifier: AssetsCollectionViewDataSource.videoCellIdentifier, for: indexPath) as! VideoCollectionViewCell 72 | let videoCell = cell as! VideoCollectionViewCell 73 | videoCell.durationLabel.text = durationFormatter.string(from: asset.duration) 74 | } else { 75 | cell = collectionView.dequeueReusableCell(withReuseIdentifier: AssetsCollectionViewDataSource.assetCellIdentifier, for: indexPath) as! AssetCollectionViewCell 76 | } 77 | UIView.setAnimationsEnabled(animationsWasEnabled) 78 | 79 | cell.accessibilityIdentifier = "Photo \(indexPath.item + 1)" 80 | cell.accessibilityTraits = UIAccessibilityTraits.button 81 | cell.isAccessibilityElement = true 82 | cell.settings = settings 83 | 84 | loadImage(for: asset, in: cell) 85 | 86 | cell.selectionIndex = store.index(of: asset) 87 | 88 | return cell 89 | } 90 | 91 | static func registerCellIdentifiersForCollectionView(_ collectionView: UICollectionView?) { 92 | collectionView?.register(AssetCollectionViewCell.self, forCellWithReuseIdentifier: assetCellIdentifier) 93 | collectionView?.register(VideoCollectionViewCell.self, forCellWithReuseIdentifier: videoCellIdentifier) 94 | } 95 | 96 | private func loadImage(for asset: PHAsset, in cell: AssetCollectionViewCell) { 97 | // Cancel any pending image requests 98 | if cell.tag != 0 { 99 | imageManager.cancelImageRequest(PHImageRequestID(cell.tag)) 100 | } 101 | 102 | // Request image 103 | cell.tag = Int(imageManager.requestImage(for: asset, targetSize: imageSize, contentMode: contentMode, options: settings.fetch.preview.photoOptions) { (image, _) in 104 | guard let image = image else { return } 105 | cell.imageView.image = image 106 | }) 107 | } 108 | } 109 | 110 | extension AssetsCollectionViewDataSource: UICollectionViewDataSourcePrefetching { 111 | func collectionView(_ collectionView: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) { 112 | let assets = indexPaths.map { fetchResult[$0.row] } 113 | imageManager.startCachingImages(for: assets, targetSize: imageSize, contentMode: contentMode, options: nil) 114 | } 115 | 116 | func collectionView(_ collectionView: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) { 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Sources/Scene/Assets/AssetsViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import UIKit 24 | import Photos 25 | 26 | protocol AssetsViewControllerDelegate: class { 27 | func assetsViewController(_ assetsViewController: AssetsViewController, didSelectAsset asset: PHAsset) 28 | func assetsViewController(_ assetsViewController: AssetsViewController, didDeselectAsset asset: PHAsset) 29 | func assetsViewController(_ assetsViewController: AssetsViewController, didLongPressCell cell: AssetCollectionViewCell, displayingAsset asset: PHAsset) 30 | } 31 | 32 | class AssetsViewController: UIViewController { 33 | weak var delegate: AssetsViewControllerDelegate? 34 | var settings: Settings! { 35 | didSet { dataSource.settings = settings } 36 | } 37 | 38 | private let store: AssetStore 39 | private let collectionView: UICollectionView = UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewFlowLayout()) 40 | private var fetchResult: PHFetchResult = PHFetchResult() { 41 | didSet { 42 | dataSource.fetchResult = fetchResult 43 | } 44 | } 45 | private let dataSource: AssetsCollectionViewDataSource 46 | 47 | private let selectionFeedback = UISelectionFeedbackGenerator() 48 | 49 | init(store: AssetStore) { 50 | self.store = store 51 | dataSource = AssetsCollectionViewDataSource(fetchResult: fetchResult, store: store) 52 | super.init(nibName: nil, bundle: nil) 53 | } 54 | 55 | required init?(coder: NSCoder) { 56 | fatalError("init(coder:) has not been implemented") 57 | } 58 | 59 | deinit { 60 | PHPhotoLibrary.shared().unregisterChangeObserver(self) 61 | } 62 | 63 | override func viewDidLoad() { 64 | super.viewDidLoad() 65 | 66 | PHPhotoLibrary.shared().register(self) 67 | 68 | view = collectionView 69 | 70 | // Set an empty title to get < back button 71 | title = " " 72 | 73 | collectionView.allowsMultipleSelection = true 74 | collectionView.bounces = true 75 | collectionView.alwaysBounceVertical = true 76 | collectionView.backgroundColor = settings.theme.backgroundColor 77 | collectionView.delegate = self 78 | collectionView.dataSource = dataSource 79 | collectionView.prefetchDataSource = dataSource 80 | AssetsCollectionViewDataSource.registerCellIdentifiersForCollectionView(collectionView) 81 | 82 | let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(AssetsViewController.collectionViewLongPressed(_:))) 83 | longPressRecognizer.minimumPressDuration = 0.5 84 | collectionView.addGestureRecognizer(longPressRecognizer) 85 | 86 | syncSelections(store.assets) 87 | } 88 | 89 | override func viewWillAppear(_ animated: Bool) { 90 | super.viewWillAppear(animated) 91 | updateCollectionViewLayout(for: traitCollection) 92 | } 93 | 94 | func showAssets(in album: PHAssetCollection) { 95 | fetchResult = PHAsset.fetchAssets(in: album, options: settings.fetch.assets.options) 96 | collectionView.reloadData() 97 | let selections = self.store.assets 98 | syncSelections(selections) 99 | collectionView.setContentOffset(.zero, animated: false) 100 | } 101 | 102 | private func syncSelections(_ assets: [PHAsset]) { 103 | collectionView.allowsMultipleSelection = true 104 | 105 | // Unselect all 106 | for indexPath in collectionView.indexPathsForSelectedItems ?? [] { 107 | collectionView.deselectItem(at: indexPath, animated: false) 108 | } 109 | 110 | // Sync selections 111 | for asset in assets { 112 | let index = fetchResult.index(of: asset) 113 | guard index != NSNotFound else { continue } 114 | let indexPath = IndexPath(item: index, section: 0) 115 | 116 | let numberOfItems = collectionView.numberOfItems(inSection: 0) 117 | guard index + 1 <= numberOfItems else { continue } 118 | 119 | collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) 120 | updateSelectionIndexForCell(at: indexPath) 121 | } 122 | } 123 | 124 | func unselect(asset: PHAsset) { 125 | let index = fetchResult.index(of: asset) 126 | guard index != NSNotFound else { return } 127 | let indexPath = IndexPath(item: index, section: 0) 128 | collectionView.deselectItem(at:indexPath, animated: false) 129 | 130 | for indexPath in collectionView.indexPathsForSelectedItems ?? [] { 131 | updateSelectionIndexForCell(at: indexPath) 132 | } 133 | } 134 | 135 | override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { 136 | super.traitCollectionDidChange(previousTraitCollection) 137 | updateCollectionViewLayout(for: traitCollection) 138 | } 139 | 140 | @objc func collectionViewLongPressed(_ sender: UILongPressGestureRecognizer) { 141 | guard settings.preview.enabled else { return } 142 | guard sender.state == .began else { return } 143 | 144 | selectionFeedback.selectionChanged() 145 | 146 | // Calculate which index path long press came from 147 | let location = sender.location(in: collectionView) 148 | guard let indexPath = collectionView.indexPathForItem(at: location) else { return } 149 | guard let cell = collectionView.cellForItem(at: indexPath) as? AssetCollectionViewCell else { return } 150 | let asset = fetchResult.object(at: indexPath.row) 151 | 152 | delegate?.assetsViewController(self, didLongPressCell: cell, displayingAsset: asset) 153 | } 154 | 155 | private func updateCollectionViewLayout(for traitCollection: UITraitCollection) { 156 | guard let collectionViewFlowLayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return } 157 | 158 | let itemSpacing = settings.list.spacing 159 | let itemsPerRow = settings.list.cellsPerRow(traitCollection.verticalSizeClass, traitCollection.horizontalSizeClass) 160 | let itemWidth = (collectionView.bounds.width - CGFloat(itemsPerRow - 1) * itemSpacing) / CGFloat(itemsPerRow) 161 | let itemSize = CGSize(width: itemWidth, height: itemWidth) 162 | 163 | collectionViewFlowLayout.minimumLineSpacing = itemSpacing 164 | collectionViewFlowLayout.minimumInteritemSpacing = itemSpacing 165 | collectionViewFlowLayout.itemSize = itemSize 166 | 167 | dataSource.imageSize = itemSize.resize(by: UIScreen.main.scale) 168 | } 169 | 170 | private func updateSelectionIndexForCell(at indexPath: IndexPath) { 171 | guard settings.theme.selectionStyle == .numbered else { return } 172 | guard let cell = collectionView.cellForItem(at: indexPath) as? AssetCollectionViewCell else { return } 173 | let asset = fetchResult.object(at: indexPath.row) 174 | cell.selectionIndex = store.index(of: asset) 175 | } 176 | } 177 | 178 | extension AssetsViewController: UICollectionViewDelegate { 179 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 180 | selectionFeedback.selectionChanged() 181 | 182 | let asset = fetchResult.object(at: indexPath.row) 183 | store.append(asset) 184 | delegate?.assetsViewController(self, didSelectAsset: asset) 185 | 186 | updateSelectionIndexForCell(at: indexPath) 187 | } 188 | 189 | func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { 190 | selectionFeedback.selectionChanged() 191 | 192 | let asset = fetchResult.object(at: indexPath.row) 193 | store.remove(asset) 194 | delegate?.assetsViewController(self, didDeselectAsset: asset) 195 | 196 | for indexPath in collectionView.indexPathsForSelectedItems ?? [] { 197 | updateSelectionIndexForCell(at: indexPath) 198 | } 199 | } 200 | 201 | func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { 202 | guard store.count < settings.selection.max || settings.selection.unselectOnReachingMax else { return false } 203 | selectionFeedback.prepare() 204 | 205 | return true 206 | } 207 | } 208 | 209 | extension AssetsViewController: PHPhotoLibraryChangeObserver { 210 | func photoLibraryDidChange(_ changeInstance: PHChange) { 211 | // Since we are gonna update UI, make sure we are on main 212 | DispatchQueue.main.async { 213 | guard let changes = changeInstance.changeDetails(for: self.fetchResult) else { return } 214 | if changes.hasIncrementalChanges { 215 | self.collectionView.performBatchUpdates({ 216 | self.fetchResult = changes.fetchResultAfterChanges 217 | 218 | // For indexes to make sense, updates must be in this order: 219 | // delete, insert, move 220 | if let removed = changes.removedIndexes, removed.count > 0 { 221 | let removedItems = removed.map { IndexPath(item: $0, section:0) } 222 | let removedSelections = self.collectionView.indexPathsForSelectedItems?.filter { return removedItems.contains($0) } 223 | removedSelections?.forEach { 224 | let removedAsset = changes.fetchResultBeforeChanges.object(at: $0.row) 225 | self.store.remove(removedAsset) 226 | self.delegate?.assetsViewController(self, didDeselectAsset: removedAsset) 227 | } 228 | self.collectionView.deleteItems(at: removedItems) 229 | } 230 | if let inserted = changes.insertedIndexes, inserted.count > 0 { 231 | self.collectionView.insertItems(at: inserted.map { IndexPath(item: $0, section:0) }) 232 | } 233 | changes.enumerateMoves { fromIndex, toIndex in 234 | self.collectionView.moveItem(at: IndexPath(item: fromIndex, section: 0), 235 | to: IndexPath(item: toIndex, section: 0)) 236 | } 237 | }) 238 | 239 | // "Use these indices to reconfigure the corresponding cells after performBatchUpdates" 240 | // https://developer.apple.com/documentation/photokit/phobjectchangedetails 241 | if let changed = changes.changedIndexes, changed.count > 0 { 242 | self.collectionView.reloadItems(at: changed.map { IndexPath(item: $0, section:0) }) 243 | } 244 | } else { 245 | self.fetchResult = changes.fetchResultAfterChanges 246 | self.collectionView.reloadData() 247 | } 248 | 249 | // No matter if we have incremental changes or not, sync the selections 250 | self.syncSelections(self.store.assets) 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /Sources/Scene/Assets/CameraCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Joakim Gyllström 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 | 23 | import UIKit 24 | import AVFoundation 25 | 26 | /** 27 | */ 28 | final class CameraCollectionViewCell: UICollectionViewCell { 29 | static let identifier = "cameraCellIdentifier" 30 | 31 | let imageView: UIImageView = UIImageView(frame: .zero) 32 | let cameraBackground: UIView = UIView(frame: .zero) 33 | 34 | var takePhotoIcon: UIImage? { 35 | didSet { 36 | imageView.image = takePhotoIcon 37 | 38 | // Apply tint to image 39 | imageView.image = imageView.image?.withRenderingMode(.alwaysTemplate) 40 | } 41 | } 42 | 43 | var session: AVCaptureSession? 44 | var captureLayer: AVCaptureVideoPreviewLayer? 45 | let sessionQueue = DispatchQueue(label: "AVCaptureVideoPreviewLayer", attributes: []) 46 | 47 | override init(frame: CGRect) { 48 | super.init(frame: frame) 49 | 50 | cameraBackground.frame = contentView.bounds 51 | cameraBackground.autoresizingMask = [.flexibleWidth, .flexibleHeight] 52 | contentView.addSubview(cameraBackground) 53 | imageView.frame = contentView.bounds 54 | imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 55 | imageView.contentMode = .center 56 | contentView.addSubview(imageView) 57 | 58 | // TODO: Check settings if live view is enabled 59 | setupCaptureLayer() 60 | } 61 | 62 | required init?(coder aDecoder: NSCoder) { 63 | fatalError("init(coder:) has not been implemented") 64 | } 65 | 66 | override func layoutSubviews() { 67 | super.layoutSubviews() 68 | 69 | captureLayer?.frame = bounds 70 | } 71 | 72 | func startLiveBackground() { 73 | sessionQueue.async { () -> Void in 74 | self.session?.startRunning() 75 | } 76 | } 77 | 78 | func stopLiveBackground() { 79 | sessionQueue.async { () -> Void in 80 | self.session?.stopRunning() 81 | } 82 | } 83 | 84 | private func setupCaptureLayer() { 85 | // Don't trigger camera access for the background 86 | guard AVCaptureDevice.authorizationStatus(for: AVMediaType.video) == .authorized else { 87 | return 88 | } 89 | 90 | do { 91 | // Prepare avcapture session 92 | session = AVCaptureSession() 93 | session?.sessionPreset = AVCaptureSession.Preset.medium 94 | 95 | // Hook upp device 96 | let device = AVCaptureDevice.default(for: AVMediaType.video) 97 | let input = try AVCaptureDeviceInput(device: device!) 98 | session?.addInput(input) 99 | 100 | // Setup capture layer 101 | 102 | guard session != nil else { 103 | return 104 | } 105 | 106 | let captureLayer = AVCaptureVideoPreviewLayer(session: session!) 107 | captureLayer.frame = bounds 108 | captureLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill 109 | cameraBackground.layer.addSublayer(captureLayer) 110 | 111 | self.captureLayer = captureLayer 112 | } catch { 113 | session = nil 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Sources/Scene/Assets/CheckmarkView.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Joakim Gyllström 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 | 23 | import UIKit 24 | 25 | class CheckmarkView: UIView { 26 | required init?(coder: NSCoder) { 27 | fatalError("init(coder:) has not been implemented") 28 | } 29 | 30 | init() { 31 | super.init(frame: .zero) 32 | } 33 | 34 | override func draw(_ rect: CGRect) { 35 | let path = UIBezierPath() 36 | path.move(to: CGPoint(x: 7, y: 12.5)) 37 | path.addLine(to: CGPoint(x: 11, y: 16)) 38 | path.addLine(to: CGPoint(x: 17.5, y: 9.5)) 39 | 40 | path.stroke() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Sources/Scene/Assets/GradientView.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import Foundation 24 | import UIKit 25 | 26 | @objc(BSImagePickerGradientView) 27 | class GradientView: UIView { 28 | override class var layerClass: AnyClass { 29 | return CAGradientLayer.self 30 | } 31 | 32 | override var layer: CAGradientLayer { 33 | return super.layer as! CAGradientLayer 34 | } 35 | 36 | var colors: [UIColor]? { 37 | get { 38 | let layerColors = layer.colors as? [CGColor] 39 | return layerColors?.map { UIColor(cgColor: $0) } 40 | } set { 41 | layer.colors = newValue?.map { $0.cgColor } 42 | } 43 | } 44 | 45 | open var locations: [NSNumber]? { 46 | get { 47 | return layer.locations 48 | } set { 49 | layer.locations = newValue 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Sources/Scene/Assets/NumberView.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2020 Joakim Gyllström 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 | 23 | import UIKit 24 | 25 | class NumberView: UILabel { 26 | 27 | override var tintColor: UIColor! { 28 | didSet { 29 | textColor = tintColor 30 | } 31 | } 32 | 33 | required init?(coder: NSCoder) { 34 | fatalError("init(coder:) has not been implemented") 35 | } 36 | 37 | init() { 38 | super.init(frame: .zero) 39 | 40 | font = UIFont.boldSystemFont(ofSize: 12) 41 | numberOfLines = 1 42 | adjustsFontSizeToFitWidth = true 43 | baselineAdjustment = .alignCenters 44 | textAlignment = .center 45 | } 46 | 47 | override func draw(_ rect: CGRect) { 48 | super.drawText(in: rect) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Sources/Scene/Assets/SelectionView.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2020 Joakim Gyllström 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 | 23 | import UIKit 24 | 25 | /** 26 | Used as an overlay on selected cells 27 | */ 28 | class SelectionView: UIView { 29 | var settings: Settings! 30 | 31 | var selectionIndex: Int? { 32 | didSet { 33 | guard let numberView = icon as? NumberView, let selectionIndex = selectionIndex else { return } 34 | // Add 1 since selections should be 1-indexed 35 | numberView.text = (selectionIndex + 1).description 36 | setNeedsDisplay() 37 | } 38 | } 39 | 40 | private lazy var icon: UIView = { 41 | switch settings.theme.selectionStyle { 42 | case .checked: 43 | return CheckmarkView() 44 | case .numbered: 45 | return NumberView() 46 | } 47 | }() 48 | 49 | override init(frame: CGRect) { 50 | super.init(frame: frame) 51 | backgroundColor = .clear 52 | } 53 | 54 | required init?(coder aDecoder: NSCoder) { 55 | super.init(coder: aDecoder) 56 | backgroundColor = .clear 57 | } 58 | 59 | override func draw(_ rect: CGRect) { 60 | //// General Declarations 61 | let context = UIGraphicsGetCurrentContext() 62 | 63 | //// Shadow Declarations 64 | let shadow2Offset = CGSize(width: 0.1, height: -0.1); 65 | let shadow2BlurRadius: CGFloat = 2.5; 66 | 67 | //// Frames 68 | let selectionFrame = bounds; 69 | 70 | //// Subframes 71 | let group = selectionFrame.insetBy(dx: 3, dy: 3) 72 | 73 | //// SelectedOval Drawing 74 | let selectedOvalPath = UIBezierPath(ovalIn: CGRect(x: group.minX + floor(group.width * 0.0 + 0.5), y: group.minY + floor(group.height * 0.0 + 0.5), width: floor(group.width * 1.0 + 0.5) - floor(group.width * 0.0 + 0.5), height: floor(group.height * 1.0 + 0.5) - floor(group.height * 0.0 + 0.5))) 75 | context?.saveGState() 76 | context?.setShadow(offset: shadow2Offset, blur: shadow2BlurRadius, color: settings.theme.selectionShadowColor.cgColor) 77 | settings.theme.selectionFillColor.setFill() 78 | selectedOvalPath.fill() 79 | context?.restoreGState() 80 | 81 | settings.theme.selectionStrokeColor.setStroke() 82 | selectedOvalPath.lineWidth = 1 83 | selectedOvalPath.stroke() 84 | 85 | //// Selection icon 86 | let largestSquareInCircleInsetRatio: CGFloat = 0.5 - (0.25 * sqrt(2)) 87 | let dx = group.size.width * largestSquareInCircleInsetRatio 88 | let dy = group.size.height * largestSquareInCircleInsetRatio 89 | icon.frame = group.insetBy(dx: dx, dy: dy) 90 | icon.tintColor = settings.theme.selectionStrokeColor 91 | icon.draw(icon.frame) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /Sources/Scene/Assets/VideoCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import UIKit 24 | 25 | class VideoCollectionViewCell: AssetCollectionViewCell { 26 | let gradientView = GradientView(frame: .zero) 27 | let durationLabel = UILabel(frame: .zero) 28 | 29 | override init(frame: CGRect) { 30 | super.init(frame: frame) 31 | 32 | gradientView.translatesAutoresizingMaskIntoConstraints = false 33 | imageView.addSubview(gradientView) 34 | gradientView.colors = [.clear, .black] 35 | gradientView.locations = [0.0 , 0.7] 36 | 37 | NSLayoutConstraint.activate([ 38 | gradientView.heightAnchor.constraint(equalToConstant: 30), 39 | gradientView.leadingAnchor.constraint(equalTo: imageView.leadingAnchor), 40 | gradientView.trailingAnchor.constraint(equalTo: imageView.trailingAnchor), 41 | gradientView.bottomAnchor.constraint(equalTo: imageView.bottomAnchor) 42 | ]) 43 | 44 | durationLabel.textAlignment = .right 45 | durationLabel.text = "0:03" 46 | durationLabel.textColor = .white 47 | durationLabel.font = UIFont.boldSystemFont(ofSize: 12) 48 | durationLabel.translatesAutoresizingMaskIntoConstraints = false 49 | gradientView.addSubview(durationLabel) 50 | 51 | NSLayoutConstraint.activate([ 52 | durationLabel.topAnchor.constraint(greaterThanOrEqualTo: gradientView.topAnchor, constant: -4), 53 | durationLabel.bottomAnchor.constraint(equalTo: gradientView.bottomAnchor, constant: -4), 54 | durationLabel.leadingAnchor.constraint(equalTo: gradientView.leadingAnchor, constant: -8), 55 | durationLabel.trailingAnchor.constraint(equalTo: gradientView.trailingAnchor, constant: -8) 56 | ]) 57 | } 58 | 59 | required init?(coder aDecoder: NSCoder) { 60 | fatalError("init(coder:) has not been implemented") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/Scene/Camera/CameraPreviewView.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import UIKit 24 | import AVFoundation 25 | 26 | class CameraPreviewView: UIView { 27 | var videoPreviewLayer: AVCaptureVideoPreviewLayer { 28 | return layer as! AVCaptureVideoPreviewLayer 29 | } 30 | 31 | var session: AVCaptureSession? { 32 | didSet { 33 | videoPreviewLayer.session = session 34 | } 35 | } 36 | 37 | override class var layerClass: AnyClass { 38 | return AVCaptureVideoPreviewLayer.self 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/Scene/Camera/CameraViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import UIKit 24 | import AVFoundation 25 | 26 | class CameraViewController: UIViewController { 27 | private let captureSession = AVCaptureSession() 28 | private let previewView = CameraPreviewView() 29 | private let captureSessionQueue = DispatchQueue(label: "session queue") 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | 34 | previewView.session = captureSession 35 | previewView.frame = view.bounds 36 | previewView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 37 | view.addSubview(previewView) 38 | 39 | requestAuthorization() 40 | setupSession() 41 | } 42 | 43 | private func requestAuthorization() { 44 | switch AVCaptureDevice.authorizationStatus(for: .video) { 45 | case .authorized: 46 | break 47 | case .notDetermined: 48 | captureSessionQueue.suspend() 49 | AVCaptureDevice.requestAccess(for: .video, completionHandler: { [weak self] granted in 50 | if granted { 51 | self?.captureSessionQueue.resume() 52 | } else { 53 | // TODO: User didn't grant access. Show something? 54 | } 55 | }) 56 | 57 | default: 58 | // TODO: User has denied access...show some sort of dialog..? 59 | break 60 | } 61 | } 62 | 63 | private func setupSession() { 64 | captureSessionQueue.async { 65 | 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/Scene/Preview/LivePreviewViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import UIKit 24 | import PhotosUI 25 | 26 | class LivePreviewViewController: PreviewViewController { 27 | private let imageManager = PHCachingImageManager.default() 28 | private let livePhotoView = PHLivePhotoView() 29 | private let badgeView = UIImageView() 30 | 31 | override var asset: PHAsset? { 32 | didSet { 33 | guard let asset = asset else { return } 34 | 35 | // Load live photo for preview 36 | let targetSize = livePhotoView.frame.size.resize(by: UIScreen.main.scale) 37 | PHCachingImageManager.default().requestLivePhoto(for: asset, targetSize: targetSize, contentMode: .aspectFit, options: settings.fetch.preview.livePhotoOptions) { [weak self] (livePhoto, _) in 38 | guard let livePhoto = livePhoto else { return } 39 | self?.livePhotoView.livePhoto = livePhoto 40 | self?.positionBadgeView(for: livePhoto) 41 | } 42 | } 43 | } 44 | 45 | override var fullscreen: Bool { 46 | didSet { 47 | UIView.animate(withDuration: 0.3) { 48 | self.badgeView.alpha = self.fullscreen ? 0 : 1 49 | } 50 | } 51 | } 52 | 53 | override func viewDidLoad() { 54 | super.viewDidLoad() 55 | 56 | livePhotoView.frame = scrollView.bounds 57 | livePhotoView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 58 | livePhotoView.contentMode = .scaleAspectFit 59 | scrollView.addSubview(livePhotoView) 60 | 61 | let badge = PHLivePhotoView.livePhotoBadgeImage(options: .overContent) 62 | badgeView.image = badge 63 | badgeView.sizeToFit() 64 | livePhotoView.addSubview(badgeView) 65 | } 66 | 67 | override func viewDidAppear(_ animated: Bool) { 68 | super.viewDidAppear(animated) 69 | imageView.isHidden = true 70 | } 71 | 72 | override func viewForZooming(in scrollView: UIScrollView) -> UIView? { 73 | return livePhotoView 74 | } 75 | 76 | private func positionBadgeView(for livePhoto: PHLivePhoto?) { 77 | guard let livePhoto = livePhoto else { 78 | badgeView.frame.origin = .zero 79 | return 80 | } 81 | 82 | let imageFrame = ImageViewLayout.frameForImageWithSize(livePhoto.size, previousFrame: .zero, inContainerWithSize: livePhotoView.frame.size, usingContentMode: .scaleAspectFit) 83 | badgeView.frame.origin = imageFrame.origin 84 | } 85 | 86 | override func viewDidLayoutSubviews() { 87 | super.viewDidLayoutSubviews() 88 | positionBadgeView(for: livePhotoView.livePhoto) 89 | } 90 | } 91 | 92 | extension LivePreviewViewController: PHLivePhotoViewDelegate { 93 | func livePhotoView(_ livePhotoView: PHLivePhotoView, willBeginPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) { 94 | // Hide badge view if we aren't in fullscreen 95 | guard fullscreen == false else { return } 96 | UIView.animate(withDuration: 0.3) { [weak self] in 97 | self?.badgeView.alpha = 0 98 | } 99 | } 100 | 101 | func livePhotoView(_ livePhotoView: PHLivePhotoView, didEndPlaybackWith playbackStyle: PHLivePhotoViewPlaybackStyle) { 102 | // Show badge view if we aren't in fullscreen 103 | guard fullscreen == false else { return } 104 | UIView.animate(withDuration: 0.3) { [weak self] in 105 | self?.badgeView.alpha = 1 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/Scene/Preview/PlayerView.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import Foundation 24 | import UIKit 25 | import AVFoundation 26 | 27 | class PlayerView: UIView { 28 | override class var layerClass: AnyClass { 29 | return AVPlayerLayer.self 30 | } 31 | 32 | override var layer: AVPlayerLayer { 33 | return super.layer as! AVPlayerLayer 34 | } 35 | 36 | var player: AVPlayer? { 37 | set { 38 | layer.player = newValue 39 | } 40 | get { 41 | return layer.player 42 | } 43 | } 44 | 45 | convenience init() { 46 | self.init(frame: .zero) 47 | } 48 | 49 | override init(frame: CGRect) { 50 | super.init(frame: frame) 51 | layer.videoGravity = .resizeAspect 52 | } 53 | 54 | required init?(coder aDecoder: NSCoder) { 55 | super.init(coder: aDecoder) 56 | layer.videoGravity = .resizeAspect 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Sources/Scene/Preview/PreviewBuilder.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import Foundation 24 | import Photos 25 | 26 | class PreviewBuilder { 27 | static func createPreviewController(for asset: PHAsset, with settings: Settings) -> PreviewViewController { 28 | switch (asset.mediaType, asset.mediaSubtypes) { 29 | case (.video, _): 30 | let vc = VideoPreviewViewController() 31 | vc.settings = settings 32 | vc.asset = asset 33 | return vc 34 | case (.image, .photoLive): 35 | let vc = LivePreviewViewController() 36 | vc.settings = settings 37 | vc.asset = asset 38 | return vc 39 | default: 40 | let vc = PreviewViewController() 41 | vc.settings = settings 42 | vc.asset = asset 43 | return vc 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Sources/Scene/Preview/PreviewTitleBuilder.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import UIKit 24 | import Photos 25 | import CoreLocation 26 | 27 | class PreviewTitleBuilder { 28 | 29 | static func titleFor(asset: PHAsset,using theme:Settings.Theme, completion: @escaping (NSAttributedString) -> Void) { 30 | if let location = asset.location { 31 | let geocoder = CLGeocoder() 32 | geocoder.reverseGeocodeLocation(location) { (placemarks, error) in 33 | if let locality = placemarks?.first?.locality { 34 | let mutableAttributedString = NSMutableAttributedString() 35 | mutableAttributedString.append(NSAttributedString(string: locality, attributes: theme.previewTitleAttributes)) 36 | 37 | if let created = asset.creationDate { 38 | let formatter = DateFormatter() 39 | formatter.dateStyle = .long 40 | formatter.timeStyle = .short 41 | let dateString = "\n" + formatter.string(from: created) 42 | 43 | mutableAttributedString.append(NSAttributedString(string: dateString, attributes: theme.previewSubtitleAttributes)) 44 | } 45 | 46 | completion(mutableAttributedString) 47 | } else if let created = asset.creationDate { 48 | completion(titleFor(date: created, using: theme)) 49 | } 50 | } 51 | } else if let created = asset.creationDate { 52 | completion(titleFor(date: created, using: theme)) 53 | } 54 | } 55 | 56 | private static func titleFor(date: Date,using theme:Settings.Theme) -> NSAttributedString { 57 | let dateFormatter = DateFormatter() 58 | dateFormatter.timeStyle = .none 59 | dateFormatter.dateStyle = .long 60 | 61 | let text = NSMutableAttributedString() 62 | 63 | text.append(NSAttributedString(string: dateFormatter.string(from: date), attributes: theme.previewTitleAttributes)) 64 | dateFormatter.timeStyle = .short 65 | dateFormatter.dateStyle = .none 66 | text.append(NSAttributedString(string: "\n" + dateFormatter.string(from: date), attributes: theme.previewSubtitleAttributes)) 67 | 68 | return text 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Scene/Preview/PreviewViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Joakim Gyllström 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 | 23 | import UIKit 24 | import Photos 25 | import CoreLocation 26 | 27 | class PreviewViewController : UIViewController { 28 | private let imageManager = PHCachingImageManager.default() 29 | var settings: Settings! 30 | 31 | var asset: PHAsset? { 32 | didSet { 33 | updateNavigationTitle() 34 | 35 | guard let asset = asset else { 36 | imageView.image = nil 37 | return 38 | } 39 | 40 | // Load image for preview 41 | imageManager.requestImage(for: asset, targetSize: CGSize(width: asset.pixelWidth, height: asset.pixelHeight), contentMode: .aspectFit, options: settings.fetch.preview.photoOptions) { [weak self] (image, _) in 42 | guard let image = image else { return } 43 | self?.imageView.image = image 44 | } 45 | } 46 | } 47 | let imageView: UIImageView = UIImageView(frame: .zero) 48 | 49 | var fullscreen = false { 50 | didSet { 51 | guard oldValue != fullscreen else { return } 52 | UIView.animate(withDuration: 0.3) { 53 | self.updateNavigationBar() 54 | self.updateStatusBar() 55 | self.updateBackgroundColor() 56 | } 57 | } 58 | } 59 | 60 | let scrollView = UIScrollView(frame: .zero) 61 | let singleTapRecognizer = UITapGestureRecognizer() 62 | let doubleTapRecognizer = UITapGestureRecognizer() 63 | private let titleLabel = UILabel(frame: .zero) 64 | 65 | override var prefersStatusBarHidden : Bool { 66 | return fullscreen 67 | } 68 | 69 | required init() { 70 | super.init(nibName: nil, bundle: nil) 71 | setupScrollView() 72 | setupImageView() 73 | setupSingleTapRecognizer() 74 | setupDoubleTapRecognizer() 75 | } 76 | 77 | required init?(coder aDecoder: NSCoder) { 78 | fatalError("init(coder:) has not been implemented") 79 | } 80 | 81 | private func setupScrollView() { 82 | scrollView.frame = view.bounds 83 | scrollView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 84 | scrollView.delegate = self 85 | scrollView.minimumZoomScale = 1 86 | scrollView.maximumZoomScale = 3 87 | if #available(iOS 11.0, *) { 88 | // Allows the imageview to be 'under' the navigation bar 89 | scrollView.contentInsetAdjustmentBehavior = .never 90 | } 91 | scrollView.showsVerticalScrollIndicator = false 92 | scrollView.showsHorizontalScrollIndicator = false 93 | view.addSubview(scrollView) 94 | } 95 | 96 | private func setupImageView() { 97 | imageView.frame = scrollView.bounds 98 | imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 99 | imageView.contentMode = .scaleAspectFit 100 | scrollView.addSubview(imageView) 101 | } 102 | 103 | private func setupSingleTapRecognizer() { 104 | singleTapRecognizer.numberOfTapsRequired = 1 105 | singleTapRecognizer.addTarget(self, action: #selector(didSingleTap(_:))) 106 | singleTapRecognizer.require(toFail: doubleTapRecognizer) 107 | view.addGestureRecognizer(singleTapRecognizer) 108 | } 109 | 110 | private func setupDoubleTapRecognizer() { 111 | doubleTapRecognizer.numberOfTapsRequired = 2 112 | doubleTapRecognizer.addTarget(self, action: #selector(didDoubleTap(_:))) 113 | view.addGestureRecognizer(doubleTapRecognizer) 114 | } 115 | 116 | private func setupTitleLabel() { 117 | titleLabel.numberOfLines = 0 118 | titleLabel.lineBreakMode = .byClipping 119 | titleLabel.textAlignment = .center 120 | navigationItem.titleView = titleLabel 121 | } 122 | 123 | override func viewDidLoad() { 124 | super.viewDidLoad() 125 | updateBackgroundColor() 126 | setupTitleLabel() 127 | } 128 | 129 | override func viewWillDisappear(_ animated: Bool) { 130 | super.viewWillDisappear(animated) 131 | fullscreen = false 132 | } 133 | 134 | private func toggleFullscreen() { 135 | fullscreen = !fullscreen 136 | } 137 | 138 | @objc func didSingleTap(_ recognizer: UIGestureRecognizer) { 139 | toggleFullscreen() 140 | } 141 | 142 | @objc func didDoubleTap(_ recognizer: UIGestureRecognizer) { 143 | if scrollView.zoomScale > 1 { 144 | scrollView.setZoomScale(1, animated: true) 145 | } else { 146 | scrollView.zoom(to: zoomRect(scale: 2, center: recognizer.location(in: recognizer.view)), animated: true) 147 | } 148 | } 149 | 150 | private func zoomRect(scale: CGFloat, center: CGPoint) -> CGRect { 151 | guard let zoomView = viewForZooming(in: scrollView) else { return .zero } 152 | let newCenter = scrollView.convert(center, from: zoomView) 153 | 154 | var zoomRect = CGRect.zero 155 | zoomRect.size.height = zoomView.frame.size.height / scale 156 | zoomRect.size.width = zoomView.frame.size.width / scale 157 | zoomRect.origin.x = newCenter.x - (zoomRect.size.width / 2.0) 158 | zoomRect.origin.y = newCenter.y - (zoomRect.size.height / 2.0) 159 | 160 | return zoomRect 161 | } 162 | 163 | private func updateNavigationBar() { 164 | navigationController?.setNavigationBarHidden(fullscreen, animated: true) 165 | } 166 | 167 | private func updateStatusBar() { 168 | self.setNeedsStatusBarAppearanceUpdate() 169 | } 170 | 171 | private func updateBackgroundColor() { 172 | let aColor: UIColor 173 | 174 | if self.fullscreen && modalPresentationStyle == .fullScreen { 175 | aColor = UIColor.black 176 | } else { 177 | aColor = UIColor.systemBackgroundColor 178 | } 179 | 180 | view.backgroundColor = aColor 181 | } 182 | 183 | private func updateNavigationTitle() { 184 | guard let asset = asset else { return } 185 | 186 | PreviewTitleBuilder.titleFor(asset: asset,using:settings.theme) { [weak self] (text) in 187 | self?.titleLabel.attributedText = text 188 | self?.titleLabel.sizeToFit() 189 | } 190 | } 191 | } 192 | 193 | extension PreviewViewController: UIScrollViewDelegate { 194 | func viewForZooming(in scrollView: UIScrollView) -> UIView? { 195 | return imageView 196 | } 197 | 198 | func scrollViewDidZoom(_ scrollView: UIScrollView) { 199 | if scrollView.zoomScale > 1 { 200 | fullscreen = true 201 | 202 | guard let image = imageView.image else { return } 203 | guard let zoomView = viewForZooming(in: scrollView) else { return } 204 | 205 | let widthRatio = zoomView.frame.width / image.size.width 206 | let heightRatio = zoomView.frame.height / image.size.height 207 | 208 | let ratio = widthRatio < heightRatio ? widthRatio:heightRatio 209 | 210 | let newWidth = image.size.width * ratio 211 | let newHeight = image.size.height * ratio 212 | 213 | let left = 0.5 * (newWidth * scrollView.zoomScale > zoomView.frame.width ? (newWidth - zoomView.frame.width) : (scrollView.frame.width - scrollView.contentSize.width)) 214 | let top = 0.5 * (newHeight * scrollView.zoomScale > zoomView.frame.height ? (newHeight - zoomView.frame.height) : (scrollView.frame.height - scrollView.contentSize.height)) 215 | 216 | scrollView.contentInset = UIEdgeInsets(top: top.rounded(), left: left.rounded(), bottom: top.rounded(), right: left.rounded()) 217 | } else { 218 | scrollView.contentInset = .zero 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /Sources/Scene/Preview/VideoPreviewViewController.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import AVFoundation 24 | import Foundation 25 | import os 26 | import Photos 27 | import UIKit 28 | 29 | class VideoPreviewViewController: PreviewViewController { 30 | private let playerView = PlayerView() 31 | private var pauseBarButton: UIBarButtonItem! 32 | private var playBarButton: UIBarButtonItem! 33 | private let imageManager = PHCachingImageManager.default() 34 | 35 | enum State { 36 | case playing 37 | case paused 38 | } 39 | 40 | override var asset: PHAsset? { 41 | didSet { 42 | guard let asset = asset, asset.mediaType == .video else { 43 | player = nil 44 | return 45 | } 46 | 47 | imageManager.requestAVAsset(forVideo: asset, options: settings.fetch.preview.videoOptions) { (avasset, audioMix, arguments) in 48 | guard let avasset = avasset as? AVURLAsset else { return } 49 | 50 | DispatchQueue.main.async { [weak self] in 51 | self?.player = AVPlayer(url: avasset.url) 52 | self?.updateState(.playing, animated: false) 53 | } 54 | } 55 | } 56 | } 57 | 58 | private var player: AVPlayer? { 59 | didSet { 60 | guard let player = player else { return } 61 | playerView.player = player 62 | 63 | NotificationCenter.default.addObserver(self, selector: #selector(reachedEnd(notification:)), name: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem) 64 | } 65 | } 66 | 67 | override func viewDidLoad() { 68 | super.viewDidLoad() 69 | 70 | try? AVAudioSession.sharedInstance().setCategory(.playback, mode: .default) 71 | 72 | pauseBarButton = UIBarButtonItem(barButtonSystemItem: .pause, target: self, action: #selector(pausePressed(sender:))) 73 | playBarButton = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(playPressed(sender:))) 74 | 75 | playerView.frame = view.bounds 76 | playerView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 77 | view.addSubview(playerView) 78 | 79 | scrollView.isUserInteractionEnabled = false 80 | doubleTapRecognizer.isEnabled = false 81 | } 82 | 83 | override func viewWillAppear(_ animated: Bool) { 84 | super.viewWillAppear(animated) 85 | playerView.isHidden = true 86 | } 87 | 88 | override func viewDidAppear(_ animated: Bool) { 89 | super.viewDidAppear(animated) 90 | playerView.isHidden = false 91 | view.sendSubviewToBack(scrollView) 92 | } 93 | 94 | override func viewWillDisappear(_ animated: Bool) { 95 | super.viewWillDisappear(animated) 96 | updateState(.paused, animated: false) 97 | playerView.isHidden = true 98 | 99 | NotificationCenter.default.removeObserver(self) 100 | } 101 | 102 | private func updateState(_ state: State, animated: Bool = true) { 103 | switch state { 104 | case .playing: 105 | navigationItem.setRightBarButton(pauseBarButton, animated: animated) 106 | player?.play() 107 | case .paused: 108 | navigationItem.setRightBarButton(playBarButton, animated: animated) 109 | player?.pause() 110 | } 111 | } 112 | 113 | // MARK: React to events 114 | @objc func playPressed(sender: UIBarButtonItem) { 115 | if player?.currentTime() == player?.currentItem?.duration { 116 | player?.seek(to: .zero) 117 | } 118 | 119 | updateState(.playing) 120 | } 121 | 122 | @objc func pausePressed(sender: UIBarButtonItem) { 123 | updateState(.paused) 124 | } 125 | 126 | @objc func reachedEnd(notification: Notification) { 127 | player?.seek(to: .zero) 128 | updateState(.paused) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Sources/View/ArrowView.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Joakim Gyllström 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 | 23 | import UIKit 24 | 25 | class ArrowView: UIView { 26 | enum ResizingBehavior { 27 | case aspectFit /// The content is proportionally resized to fit into the target rectangle. 28 | case aspectFill /// The content is proportionally resized to completely fill the target rectangle. 29 | case stretch /// The content is stretched to match the entire target rectangle. 30 | case center /// The content is centered in the target rectangle, but it is NOT resized. 31 | 32 | func apply(rect: CGRect, target: CGRect) -> CGRect { 33 | if rect == target || target == CGRect.zero { 34 | return rect 35 | } 36 | 37 | var scales = CGSize.zero 38 | scales.width = abs(target.width / rect.width) 39 | scales.height = abs(target.height / rect.height) 40 | 41 | switch self { 42 | case .aspectFit: 43 | scales.width = min(scales.width, scales.height) 44 | scales.height = scales.width 45 | case .aspectFill: 46 | scales.width = max(scales.width, scales.height) 47 | scales.height = scales.width 48 | case .stretch: 49 | break 50 | case .center: 51 | scales.width = 1 52 | scales.height = 1 53 | } 54 | 55 | var result = rect.standardized 56 | result.size.width *= scales.width 57 | result.size.height *= scales.height 58 | result.origin.x = target.minX + (target.width - result.width) / 2 59 | result.origin.y = target.minY + (target.height - result.height) / 2 60 | return result 61 | } 62 | } 63 | 64 | var resizing: ResizingBehavior = .aspectFit 65 | var strokeColor: UIColor = .systemStrokeColor 66 | 67 | override var intrinsicContentSize: CGSize { 68 | return CGSize(width: 8, height: 8) 69 | } 70 | 71 | var asImage: UIImage { 72 | let renderer = UIGraphicsImageRenderer(bounds: bounds) 73 | return renderer.image { rendererContext in 74 | layer.render(in: rendererContext.cgContext) 75 | } 76 | } 77 | 78 | override func draw(_ rect: CGRect) { 79 | // Get graphics context 80 | let context = UIGraphicsGetCurrentContext()! 81 | 82 | // Resize to Target Frame 83 | context.saveGState() 84 | let resizedFrame: CGRect = resizing.apply(rect: CGRect(x: 0, y: 0, width: 8, height: 8), target: rect) 85 | context.translateBy(x: resizedFrame.minX, y: resizedFrame.minY) 86 | context.scaleBy(x: resizedFrame.width / 8, y: resizedFrame.height / 8) 87 | 88 | // Draw shape 89 | let path = chevronPath() 90 | strokeColor.setStroke() 91 | UIColor.white.setFill() 92 | path.lineWidth = 2 93 | path.stroke() 94 | 95 | context.restoreGState() 96 | } 97 | 98 | func chevronPath() -> UIBezierPath { 99 | let chevronPath = UIBezierPath() 100 | chevronPath.move(to: CGPoint(x: 0, y: 2)) 101 | chevronPath.addLine(to: CGPoint(x: 4, y: 6)) 102 | chevronPath.addLine(to: CGPoint(x: 8, y: 2)) 103 | 104 | return chevronPath 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Sources/View/CGSize+Scale.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Daisuke Fujiwara 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 | 23 | import UIKit 24 | 25 | extension CGSize { 26 | func resize(by scale: CGFloat) -> CGSize { 27 | return CGSize(width: self.width * scale, height: self.height * scale) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/View/ImageView.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2018 Joakim Gyllström 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 | 23 | import UIKit 24 | 25 | @IBDesignable 26 | public class ImageView: UIView { 27 | private let imageView: UIImageView = UIImageView(frame: .zero) 28 | 29 | override public var isUserInteractionEnabled: Bool { 30 | didSet { imageView.isUserInteractionEnabled = isUserInteractionEnabled } 31 | } 32 | 33 | override public var tintColor: UIColor! { 34 | didSet { imageView.tintColor = tintColor } 35 | } 36 | 37 | override public var contentMode: UIView.ContentMode { 38 | didSet { 39 | setNeedsLayout() 40 | layoutIfNeeded() 41 | } 42 | } 43 | 44 | override public init(frame: CGRect) { 45 | super.init(frame: frame) 46 | addSubview(imageView) 47 | } 48 | 49 | required public init?(coder aDecoder: NSCoder) { 50 | super.init(coder: aDecoder) 51 | addSubview(imageView) 52 | } 53 | 54 | override public func layoutSubviews() { 55 | super.layoutSubviews() 56 | 57 | if let image = imageView.image { 58 | imageView.frame = ImageViewLayout.frameForImageWithSize(image.size, previousFrame: imageView.frame, inContainerWithSize: bounds.size, usingContentMode: contentMode) 59 | } else { 60 | imageView.frame = .zero 61 | } 62 | } 63 | } 64 | 65 | // MARK: UIImageView API 66 | extension ImageView { 67 | /// See UIImageView documentation 68 | public convenience init(image: UIImage?) { 69 | self.init(frame: .zero) 70 | imageView.image = image 71 | } 72 | 73 | /// See UIImageView documentation 74 | public convenience init(image: UIImage?, highlightedImage: UIImage?) { 75 | self.init(frame: .zero) 76 | imageView.image = image 77 | imageView.highlightedImage = highlightedImage 78 | } 79 | 80 | /// See UIImageView documentation 81 | @IBInspectable 82 | open var image: UIImage? { 83 | get { return imageView.image } 84 | set { 85 | imageView.image = newValue 86 | setNeedsLayout() 87 | layoutIfNeeded() 88 | } 89 | } 90 | 91 | /// See UIImageView documentation 92 | @IBInspectable 93 | open var highlightedImage: UIImage? { 94 | get { return imageView.highlightedImage } 95 | set { 96 | imageView.highlightedImage = newValue 97 | } 98 | } 99 | 100 | /// See UIImageView documentation 101 | @IBInspectable 102 | open var isHighlighted: Bool { 103 | get { return imageView.isHighlighted } 104 | set { imageView.isHighlighted = newValue } 105 | } 106 | 107 | /// See UIImageView documentation 108 | open var animationImages: [UIImage]? { 109 | get { return imageView.animationImages } 110 | set { imageView.animationImages = newValue } 111 | } 112 | 113 | /// See UIImageView documentation 114 | open var highlightedAnimationImages: [UIImage]? { 115 | get { return imageView.highlightedAnimationImages } 116 | set { imageView.highlightedAnimationImages = newValue } 117 | } 118 | 119 | /// See UIImageView documentation 120 | open var animationDuration: TimeInterval { 121 | get { return imageView.animationDuration } 122 | set { imageView.animationDuration = newValue } 123 | } 124 | 125 | /// See UIImageView documentation 126 | open var animationRepeatCount: Int { 127 | get { return imageView.animationRepeatCount } 128 | set { imageView.animationRepeatCount = newValue } 129 | } 130 | 131 | /// See UIImageView documentation 132 | open func startAnimating() { 133 | imageView.startAnimating() 134 | } 135 | 136 | /// See UIImageView documentation 137 | open func stopAnimating() { 138 | imageView.stopAnimating() 139 | } 140 | 141 | /// See UIImageView documentation 142 | open var isAnimating: Bool { 143 | get { return imageView.isAnimating } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Sources/View/ImageViewLayout.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2018 Joakim Gyllström 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 | 23 | import UIKit 24 | 25 | struct ImageViewLayout { 26 | static func frameForImageWithSize(_ image: CGSize, previousFrame: CGRect, inContainerWithSize container: CGSize, usingContentMode contentMode: UIView.ContentMode) -> CGRect { 27 | let size = sizeForImage(image, previousSize: previousFrame.size, container: container, contentMode: contentMode) 28 | let position = positionForImage(size, previousPosition: previousFrame.origin, container: container, contentMode: contentMode) 29 | 30 | return CGRect(origin: position, size: size) 31 | } 32 | 33 | private static func sizeForImage(_ image: CGSize, previousSize: CGSize, container: CGSize, contentMode: UIView.ContentMode) -> CGSize { 34 | switch contentMode { 35 | case .scaleToFill: 36 | return container 37 | case .scaleAspectFit: 38 | let heightRatio = imageHeightRatio(image, container: container) 39 | let widthRatio = imageWidthRatio(image, container: container) 40 | return scaledImageSize(image, ratio: max(heightRatio, widthRatio)) 41 | case .scaleAspectFill: 42 | let heightRatio = imageHeightRatio(image, container: container) 43 | let widthRatio = imageWidthRatio(image, container: container) 44 | return scaledImageSize(image, ratio: min(heightRatio, widthRatio)) 45 | case .redraw: 46 | return previousSize 47 | default: 48 | return image 49 | } 50 | } 51 | 52 | private static func positionForImage(_ image: CGSize, previousPosition: CGPoint, container: CGSize, contentMode: UIView.ContentMode) -> CGPoint { 53 | switch contentMode { 54 | case .scaleToFill: 55 | return .zero 56 | case .scaleAspectFit: 57 | return CGPoint(x: (container.width - image.width) / 2, y: (container.height - image.height) / 2) 58 | case .scaleAspectFill: 59 | return CGPoint(x: (container.width - image.width) / 2, y: (container.height - image.height) / 2) 60 | case .redraw: 61 | return previousPosition 62 | case .center: 63 | return CGPoint(x: (container.width - image.width) / 2, y: (container.height - image.height) / 2) 64 | case .top: 65 | return CGPoint(x: (container.width - image.width) / 2, y: 0) 66 | case .bottom: 67 | return CGPoint(x: (container.width - image.width) / 2, y: container.height - image.height) 68 | case .left: 69 | return CGPoint(x: 0, y: (container.height - image.height) / 2) 70 | case .right: 71 | return CGPoint(x: container.width - image.width, y: (container.height - image.height) / 2) 72 | case .topLeft: 73 | return .zero 74 | case .topRight: 75 | return CGPoint(x: container.width - image.width, y: 0) 76 | case .bottomLeft: 77 | return CGPoint(x: 0, y: container.height - image.height) 78 | case .bottomRight: 79 | return CGPoint(x: container.width - image.width, y: container.height - image.height) 80 | @unknown default: 81 | return .zero 82 | } 83 | } 84 | 85 | private static func imageHeightRatio(_ image: CGSize, container: CGSize) -> CGFloat { 86 | return image.height / container.height 87 | } 88 | 89 | private static func imageWidthRatio(_ image: CGSize, container: CGSize) -> CGFloat { 90 | return image.width / container.width 91 | } 92 | 93 | private static func scaledImageSize(_ image: CGSize, ratio: CGFloat) -> CGSize { 94 | return CGSize(width: image.width / ratio, height: image.height / ratio) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Tests/CGSizeExtensionTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Daisuke Fujiwara 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 | 23 | import XCTest 24 | @testable import BSImagePicker 25 | 26 | class CGSizeExtensionTests: XCTestCase { 27 | 28 | func testResizeWithScale() { 29 | let scaledSize = CGSize(width: 10, height: 10).resize(by: 2) 30 | XCTAssertEqual(scaledSize, CGSize(width: 20, height: 20)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/CameraDataSourceTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Joakim Gyllström 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 | 23 | import Foundation 24 | import XCTest 25 | import UIKit 26 | @testable import BSImagePicker 27 | 28 | class CameraDataSourceTests: XCTestCase { 29 | } 30 | -------------------------------------------------------------------------------- /Tests/ImagePickerViewTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Joakim Gyllström 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 | 23 | import XCTest 24 | 25 | class ImagePickerViewTests: XCTestCase { 26 | 27 | func testNothing() { 28 | XCTAssert(true, "add view tests") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/SettingsTests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2019 Daisuke Fujiwara 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 | 23 | @testable import BSImagePicker 24 | import Photos 25 | import XCTest 26 | 27 | class SettingsTests: XCTestCase { 28 | var settings: Settings! 29 | 30 | override func setUp() { 31 | settings = Settings() 32 | } 33 | 34 | func testImageOnlyAssets() { 35 | settings.fetch.assets.supportedMediaTypes = [.image] 36 | let fetchOptions = settings.fetch.assets.options 37 | XCTAssertEqual(fetchOptions.predicate, NSPredicate(format: "mediaType IN %@", [PHAssetMediaType.image.rawValue])) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /UITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /UITests/UITests.swift: -------------------------------------------------------------------------------- 1 | // The MIT License (MIT) 2 | // 3 | // Copyright (c) 2015 Joakim Gyllström 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 | 23 | import XCTest 24 | import Photos 25 | 26 | class UITests: XCTestCase { 27 | 28 | let app = XCUIApplication() 29 | 30 | override func setUp() { 31 | super.setUp() 32 | 33 | // Put setup code here. This method is called before the invocation of each test method in the class. 34 | 35 | // In UI tests it is usually best to stop immediately when a failure occurs. 36 | continueAfterFailure = false 37 | // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. 38 | app.launch() 39 | 40 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 41 | } 42 | 43 | override func tearDown() { 44 | // Put teardown code here. This method is called after the invocation of each test method in the class. 45 | app.terminate() 46 | super.tearDown() 47 | } 48 | 49 | func testDoneButtonDisabledOnStartup() { 50 | app.buttons["Image picker"].tap() 51 | XCTAssert(app.navigationBars.buttons["Done"].isEnabled == false) 52 | } 53 | 54 | func _testDoneButtonEnabledAfterSelection() { 55 | app.buttons["Image picker"].tap() 56 | app.collectionViews.children(matching: .any).element(boundBy: 0).tap() 57 | XCTAssert(app.navigationBars.buttons["Done (1)"].isEnabled == true) 58 | } 59 | 60 | func _testDoneButtonDisabledAfterDeselection() { 61 | app.buttons["Image picker"].tap() 62 | app.collectionViews.children(matching: .any).element(boundBy: 0).tap() 63 | app.collectionViews.children(matching: .any).element(boundBy: 0).tap() 64 | XCTAssert(app.navigationBars.buttons["Done"].isEnabled == false) 65 | } 66 | 67 | func _testLongpressPreview() { 68 | app.buttons["Image picker"].tap() 69 | app.collectionViews.children(matching: .any).element(boundBy: 0).press(forDuration: 1.0) 70 | app.navigationBars.buttons["Back"].tap() 71 | XCTAssert(app.navigationBars.buttons["Done"].isEnabled == false) 72 | } 73 | } 74 | --------------------------------------------------------------------------------