├── .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 | [](https://travis-ci.org/mikaoj/BSImagePicker)
3 | [](http://cocoapods.org/pods/BSImagePicker)
4 | [](http://cocoapods.org/pods/BSImagePicker)
5 | [](http://cocoapods.org/pods/BSImagePicker)
6 | [](https://github.com/Carthage/Carthage)
7 |
8 | 
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 |
--------------------------------------------------------------------------------