├── .DS_Store
├── Assets
├── Demo.png
└── .DS_Store
├── Example
├── .DS_Store
└── Athlee-ImagePicker
│ ├── .DS_Store
│ ├── Athlee-ImagePicker
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── .DS_Store
│ │ ├── Flash.imageset
│ │ │ ├── Flash.png
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ ├── .DS_Store
│ │ │ └── Contents.json
│ │ ├── FlashOff.imageset
│ │ │ ├── No flash.png
│ │ │ └── Contents.json
│ │ ├── FlashAuto.imageset
│ │ │ ├── Automatic.png
│ │ │ └── Contents.json
│ │ ├── FlipCamera.imageset
│ │ │ ├── FlipCamera.png
│ │ │ └── Contents.json
│ │ ├── TakePhotoIcon.imageset
│ │ │ ├── TakePhotoIcon.png
│ │ │ └── Contents.json
│ │ └── default-user-image.imageset
│ │ │ ├── default-user-image.png
│ │ │ └── Contents.json
│ ├── .DS_Store
│ ├── ContainerType.swift
│ ├── AppDelegate.swift
│ ├── SelectionViewController.swift
│ ├── PhotoCell.swift
│ ├── Info.plist
│ ├── HolderViewController.swift
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── CaptureViewController.swift
│ ├── ImagePickerController.swift
│ ├── CropViewController.swift
│ └── PhotoViewController.swift
│ ├── Athlee-ImagePicker.xcodeproj
│ ├── project.xcworkspace
│ │ └── contents.xcworkspacedata
│ └── project.pbxproj
│ └── Athlee-ImagePickerTests
│ ├── Info.plist
│ └── Athlee_ImagePickerTests.swift
├── Source
├── .DS_Store
├── Radians.swift
├── NSIndexSet.swift
├── UIView.swift
├── PhotoFetchable.swift
├── UICollectionView.swift
├── CaptureNotificationObserver.swift
├── CGAffineTransform.swift
├── CGRect.swift
├── LinesView.swift
├── CollectionViewChangeObserver.swift
├── CropableScrollViewDelegate.swift
├── PhotoCapturable.swift
├── PhotoCachable.swift
├── Cropable.swift
├── Capturable.swift
└── FloatingViewLayout.swift
├── ImagePickerKit.podspec
├── LICENSE
├── README.md
└── .gitignore
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Athlee/ImagePickerKit/HEAD/.DS_Store
--------------------------------------------------------------------------------
/Assets/Demo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Athlee/ImagePickerKit/HEAD/Assets/Demo.png
--------------------------------------------------------------------------------
/Assets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Athlee/ImagePickerKit/HEAD/Assets/.DS_Store
--------------------------------------------------------------------------------
/Example/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Athlee/ImagePickerKit/HEAD/Example/.DS_Store
--------------------------------------------------------------------------------
/Source/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Athlee/ImagePickerKit/HEAD/Source/.DS_Store
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Athlee/ImagePickerKit/HEAD/Example/Athlee-ImagePicker/.DS_Store
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Athlee/ImagePickerKit/HEAD/Example/Athlee-ImagePicker/Athlee-ImagePicker/.DS_Store
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Athlee/ImagePickerKit/HEAD/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/.DS_Store
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/Flash.imageset/Flash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Athlee/ImagePickerKit/HEAD/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/Flash.imageset/Flash.png
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/AppIcon.appiconset/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Athlee/ImagePickerKit/HEAD/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/AppIcon.appiconset/.DS_Store
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/FlashOff.imageset/No flash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Athlee/ImagePickerKit/HEAD/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/FlashOff.imageset/No flash.png
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/FlashAuto.imageset/Automatic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Athlee/ImagePickerKit/HEAD/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/FlashAuto.imageset/Automatic.png
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/FlipCamera.imageset/FlipCamera.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Athlee/ImagePickerKit/HEAD/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/FlipCamera.imageset/FlipCamera.png
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/TakePhotoIcon.imageset/TakePhotoIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Athlee/ImagePickerKit/HEAD/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/TakePhotoIcon.imageset/TakePhotoIcon.png
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/default-user-image.imageset/default-user-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Athlee/ImagePickerKit/HEAD/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/default-user-image.imageset/default-user-image.png
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/ContainerType.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContainerType.swift
3 | // Athlee-ImagePicker
4 | //
5 | // Created by mac on 15/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | protocol ContainerType {
12 | associatedtype ParentType
13 | var parent: ParentType { get set }
14 | }
15 |
--------------------------------------------------------------------------------
/Source/Radians.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Radians.swift
3 | // Pods
4 | //
5 | // Created by mac on 27/11/2016.
6 | //
7 | //
8 |
9 | import Foundation
10 |
11 | public extension Double {
12 | public func toRadians() -> Double {
13 | return self * .pi / 180.0
14 | }
15 | }
16 |
17 | public extension Float {
18 | public func toRadians() -> Float {
19 | return self * .pi / 180
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/Flash.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "Flash.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/FlashAuto.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "Automatic.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/FlashOff.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "No flash.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/FlipCamera.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "FlipCamera.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/TakePhotoIcon.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "TakePhotoIcon.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/default-user-image.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "scale" : "1x"
6 | },
7 | {
8 | "idiom" : "universal",
9 | "filename" : "default-user-image.png",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "version" : 1,
19 | "author" : "xcode"
20 | }
21 | }
--------------------------------------------------------------------------------
/Source/NSIndexSet.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NSIndexSet.swift
3 | // Cropable
4 | //
5 | // Created by mac on 15/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import Foundation
10 |
11 | internal extension IndexSet {
12 | func indexPaths(from section: Int) -> [IndexPath] {
13 | var indexPaths: [IndexPath] = []
14 | indexPaths.reserveCapacity(count)
15 |
16 | forEach {
17 | indexPaths.append(IndexPath(item: $0, section: section))
18 | }
19 |
20 | return indexPaths
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Source/UIView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UIView.swift
3 | // Athlee-ImagePicker
4 | //
5 | // Created by mac on 15/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | internal extension UIView {
12 | func snapshot() -> UIImage {
13 | UIGraphicsBeginImageContextWithOptions(frame.size, true, 0)
14 | drawHierarchy(in: bounds, afterScreenUpdates: false)
15 | let image = UIGraphicsGetImageFromCurrentImageContext()
16 | UIGraphicsEndImageContext()
17 |
18 | return image!
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // Athlee-ImagePicker
4 | //
5 | // Created by mac on 13/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 | var window: UIWindow?
14 |
15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
16 | return true
17 | }
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/Source/PhotoFetchable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoFetcher.swift
3 | // Cropable
4 | //
5 | // Created by mac on 15/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Photos
11 |
12 | ///
13 | /// Provides photos' fetching features.
14 | ///
15 | public protocol PhotoFetchable {
16 | /// Current fetch result object.
17 | var fetchResult: PHFetchResult { get set }
18 |
19 | ///
20 | /// Checks if a user has given permission to use
21 | /// her photo assets for the app.
22 | ///
23 | func checkPhotoAuth()
24 | }
25 |
--------------------------------------------------------------------------------
/Source/UICollectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UICollectionView.swift
3 | // Cropable
4 | //
5 | // Created by mac on 15/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | internal extension UICollectionView {
12 | func indexPaths(for rect: CGRect) -> [IndexPath] {
13 | guard let allLayoutAttributes = collectionViewLayout.layoutAttributesForElements(in: rect) else {
14 | return []
15 | }
16 |
17 | guard allLayoutAttributes.count > 0 else {
18 | return []
19 | }
20 |
21 | let indexPaths = allLayoutAttributes.map { $0.indexPath }
22 |
23 | return indexPaths
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/ImagePickerKit.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |s|
2 |
3 | s.name = "ImagePickerKit"
4 | s.version = "0.2.8"
5 | s.summary = "ImagePickerKit is a protocol-oriented framework that provides handly features to dealing with picking or taking a photo!"
6 | s.homepage = "https://github.com/Athlee/ImagePickerKit"
7 | s.license = { :type => "MIT", :file => "LICENSE" }
8 | s.author = { "Eugene Mozharovsky" => "mozharovsky@live.com" }
9 | s.social_media_url = "http://twitter.com/dottieyottie"
10 | s.platform = :ios, "9.0"
11 | s.ios.deployment_target = "9.0"
12 | s.source = { :git => "https://github.com/Athlee/ImagePickerKit.git", :tag => s.version }
13 | s.source_files = "Source/*.swift"
14 | s.requires_arc = true
15 |
16 | end
17 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePickerTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
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 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/SelectionViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SelectionViewController.swift
3 | // Athlee-ImagePicker
4 | //
5 | // Created by mac on 15/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class SelectionViewController: UIViewController {
12 |
13 | // MARK: Outlets
14 |
15 | @IBOutlet weak var imageView: UIImageView!
16 |
17 | // MARK: Life cycle
18 |
19 | override func viewDidLoad() {
20 | super.viewDidLoad()
21 | }
22 |
23 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
24 | let dest = segue.destination
25 | if let dest = dest as? UINavigationController, let holder = dest.topViewController as? HolderViewController {
26 | holder._parent = self
27 | } else if let dest = dest as? CaptureViewController {
28 | dest._parent = self
29 | }
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | }
43 | ],
44 | "info" : {
45 | "version" : 1,
46 | "author" : "xcode"
47 | }
48 | }
--------------------------------------------------------------------------------
/Source/CaptureNotificationObserver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureNotificationObserver.swift
3 | // Athlee-ImagePicker
4 | //
5 | // Created by mac on 16/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | ///
12 | /// A default observer around capture notifications.
13 | ///
14 | open class CaptureNotificationObserver: NSObject {
15 | fileprivate unowned var capturable: T
16 |
17 | public init(capturable: T) {
18 | self.capturable = capturable
19 | }
20 |
21 | open func register() {
22 | let notificationCenter = NotificationCenter.default
23 | notificationCenter.addObserver(
24 | self,
25 | selector: #selector(CaptureNotificationObserver.willEnterForegroundNotification(_:)),
26 | name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil
27 | )
28 | }
29 |
30 | open func unregister() {
31 | NotificationCenter.default.removeObserver(self)
32 | }
33 |
34 | open func willEnterForegroundNotification(_ notification: Notification) {
35 | capturable.willEnterForegroundNotification(notification)
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Athlee
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 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePickerTests/Athlee_ImagePickerTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Athlee_ImagePickerTests.swift
3 | // Athlee-ImagePickerTests
4 | //
5 | // Created by mac on 13/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import Athlee_ImagePicker
11 |
12 | class Athlee_ImagePickerTests: XCTestCase {
13 |
14 | override func setUp() {
15 | super.setUp()
16 | // Put setup code here. This method is called before the invocation of each test method in the class.
17 | }
18 |
19 | override func tearDown() {
20 | // Put teardown code here. This method is called after the invocation of each test method in the class.
21 | super.tearDown()
22 | }
23 |
24 | func testExample() {
25 | // This is an example of a functional test case.
26 | // Use XCTAssert and related functions to verify your tests produce the correct results.
27 | }
28 |
29 | func testPerformanceExample() {
30 | // This is an example of a performance test case.
31 | self.measure {
32 | // Put the code you want to measure the time of here.
33 | }
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/PhotoCell.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoCell.swift
3 | // Athlee-ImagePicker
4 | //
5 | // Created by mac on 15/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class PhotoCell: UICollectionViewCell {
12 |
13 | @IBOutlet weak var photoImageView: UIImageView!
14 |
15 | let overlayView = UIView()
16 |
17 | override func awakeFromNib() {
18 | super.awakeFromNib()
19 |
20 | overlayView.translatesAutoresizingMaskIntoConstraints = false
21 | overlayView.backgroundColor = UIColor.black
22 | overlayView.alpha = 0
23 |
24 | addSubview(overlayView)
25 |
26 | let anchors = [
27 | overlayView.topAnchor.constraint(equalTo: topAnchor),
28 | overlayView.bottomAnchor.constraint(equalTo: bottomAnchor),
29 | overlayView.leadingAnchor.constraint(equalTo: leadingAnchor),
30 | overlayView.trailingAnchor.constraint(equalTo: trailingAnchor)
31 | ].flatMap { $0 }
32 |
33 | NSLayoutConstraint.activate(anchors)
34 | }
35 |
36 | override var isSelected: Bool {
37 | didSet {
38 | if isSelected {
39 | overlayView.alpha = 0.6
40 | } else {
41 | overlayView.alpha = 0
42 | }
43 | }
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ImagePickerKit
2 |
3 |
4 |
5 |
6 |
7 | `ImagePickerKit` is a protocol-oriented framework that provides handly features to dealing with picking or taking a photo! Image selection works just as in the Instagram and taking photos made easy. Moreover it is built entirely on protocols which gives an incredible flexibility.
8 |
9 | ### Features
10 |
11 | - [x] Instagram-like floating image container
12 | - [x] Cropping selected photo
13 | - [x] Taking photos
14 | - [x] Easy setup
15 |
16 | # Installation
17 | ### CocoaPods
18 |
19 | `ImagePickerKit` is available for installation using the [CocoaPods](https://cocoapods.org).
20 |
21 | Add the following code to your `Podfile`:
22 | ```ruby
23 | pod 'ImagePickerKit'
24 | ```
25 |
26 | # Usage
27 |
28 | // TODO:
29 |
30 | # Community
31 | * For help & feedback please use [issues](https://github.com/Athlee/ImagePickerKit/issues).
32 | * Got a new feature? Please submit a [pull request](https://github.com/Athlee/ImagePickerKit/pulls).
33 | * Email us with urgent queries.
34 |
35 | # License
36 | `ImagePicker` is available under the MIT license, see the [LICENSE](https://github.com/Athlee/ImagePickerKit/blob/master/LICENSE) file for more information.
37 |
--------------------------------------------------------------------------------
/Source/CGAffineTransform.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGAffineTransform.swift
3 | // Pods
4 | //
5 | // Created by mac on 27/11/2016.
6 | //
7 | //
8 |
9 | import UIKit
10 |
11 | public extension CGAffineTransform {
12 | public static func scalingFactor(toFill containerSize: CGSize, with contentSize: CGSize, atAngle angle: Double) -> Double {
13 | var theta = fabs(angle - 2 * .pi * trunc(angle / .pi / 2) - .pi)
14 |
15 | if theta > .pi / 2 {
16 | theta = fabs(.pi - theta)
17 | }
18 |
19 | let h = Double(contentSize.height)
20 | let H = Double(containerSize.height)
21 | let w = Double(contentSize.width)
22 | let W = Double(containerSize.width)
23 |
24 | let scale1 = (H * cos(theta) + W * sin(theta)) / min(H, h)
25 | let scale2 = (H * sin(theta) + W * cos(theta)) / min(W, w)
26 |
27 | let scalingFactor = max(scale1, scale2)
28 |
29 | return scalingFactor
30 | }
31 |
32 | func scaling(toFill containerSize: CGSize, with contentSize: CGSize, atAngle angle: Double) -> CGAffineTransform {
33 | let factor = CGFloat(CGAffineTransform.scalingFactor(toFill: containerSize,
34 | with: contentSize,
35 | atAngle: angle))
36 |
37 | return self.scaledBy(x: factor, y: factor)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Source/CGRect.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CGRect.swift
3 | // Cropable
4 | //
5 | // Created by mac on 15/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | internal extension CGRect {
12 | enum Difference {
13 | case added(area: CGRect)
14 | case removed(area: CGRect)
15 | }
16 |
17 | // TODO: Make this function accurate in case of Geometry.
18 | func exclusiveOr(_ rect: CGRect) -> [CGRect] {
19 | var res: [CGRect] = []
20 |
21 | if rect.maxY > self.maxY {
22 | let x = max(rect.origin.x, self.origin.x)
23 | let bottomExtraRect = CGRect(
24 | origin: CGPoint(x: x, y: self.maxY),
25 | size: CGSize(width: rect.width, height: rect.maxY - self.maxY)
26 | )
27 |
28 | res.append(bottomExtraRect)
29 | }
30 |
31 | if self.minY > rect.minY {
32 | let x = max(rect.origin.x, self.origin.x)
33 | let topExtraRect = CGRect(
34 | origin: CGPoint(x: x, y: rect.minY),
35 | size: CGSize(width: rect.width, height: self.minY - rect.minY)
36 | )
37 |
38 | res.append(topExtraRect)
39 | }
40 |
41 | return res
42 | }
43 |
44 | func difference(with rect: CGRect) -> [Difference] {
45 | guard intersects(rect) else {
46 | return [.added(area: rect), .removed(area: self)]
47 | }
48 |
49 | let added = exclusiveOr(rect).map { Difference.added(area: $0) }
50 | let removed = rect.exclusiveOr(self).map { Difference.removed(area: $0) }
51 |
52 | return added + removed
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/Source/LinesView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LinesView.swift
3 | // Cropable
4 | //
5 | // Created by mac on 14/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | ///
12 | /// Draws the horizontal and vertical lines to show
13 | /// that a cropping is happening right now.
14 | ///
15 | open class LinesView: UIView {
16 | open var lines = 3 { didSet { setNeedsDisplay() } }
17 | open var columns = 3 { didSet { setNeedsDisplay() } }
18 | open var width: CGFloat = 1 { didSet { setNeedsDisplay() } }
19 | open var color = UIColor.white { didSet { setNeedsDisplay() } }
20 |
21 | override open func draw(_ rect: CGRect) {
22 | let verticalSpace = rect.height / CGFloat(lines)
23 | let horizontalSpace = rect.width / CGFloat(columns)
24 |
25 | for i in 1..
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | en
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 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1
23 | LSRequiresIPhoneOS
24 |
25 | NSPhotoLibraryUsageDescription
26 | Please allow us using your photos!
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 | NSCameraUsageDescription
42 | Please allow us using your camera!
43 |
44 |
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## Build generated
6 | build/
7 | DerivedData/
8 |
9 | ## Various settings
10 | *.pbxuser
11 | !default.pbxuser
12 | *.mode1v3
13 | !default.mode1v3
14 | *.mode2v3
15 | !default.mode2v3
16 | *.perspectivev3
17 | !default.perspectivev3
18 | xcuserdata/
19 |
20 | ## Other
21 | *.moved-aside
22 | *.xcuserstate
23 |
24 | ## Obj-C/Swift specific
25 | *.hmap
26 | *.ipa
27 | *.dSYM.zip
28 | *.dSYM
29 |
30 | ## Playgrounds
31 | timeline.xctimeline
32 | playground.xcworkspace
33 |
34 | # Swift Package Manager
35 | #
36 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
37 | # Packages/
38 | .build/
39 |
40 | # CocoaPods
41 | #
42 | # We recommend against adding the Pods directory to your .gitignore. However
43 | # you should judge for yourself, the pros and cons are mentioned at:
44 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
45 | #
46 | # Pods/
47 |
48 | # Carthage
49 | #
50 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
51 | # Carthage/Checkouts
52 |
53 | Carthage/Build
54 |
55 | # fastlane
56 | #
57 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
58 | # screenshots whenever they are needed.
59 | # For more information about the recommended setup visit:
60 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
61 |
62 | fastlane/report.xml
63 | fastlane/Preview.html
64 | fastlane/screenshots
65 | fastlane/test_output
66 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/HolderViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HolderViewController.swift
3 | // Athlee-ImagePicker
4 | //
5 | // Created by mac on 15/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class HolderViewController: UIViewController {
12 |
13 | // MARK: Outlets
14 |
15 | @IBOutlet weak var topConstraint: NSLayoutConstraint!
16 | @IBOutlet weak var topContainer: UIView!
17 |
18 | // MARK: Properties
19 |
20 | var _parent: SelectionViewController!
21 |
22 | var cropViewController: CropViewController!
23 | var photoViewController: PhotoViewController!
24 |
25 | var image: UIImage? {
26 | didSet {
27 | guard let image = image else { return }
28 | for child in childViewControllers {
29 | if let child = child as? CropViewController {
30 | child.addImage(image)
31 | }
32 | }
33 | }
34 | }
35 |
36 | // MARK: Life cycle
37 |
38 | override func viewDidLoad() {
39 | super.viewDidLoad()
40 |
41 | for child in childViewControllers {
42 | if let child = child as? CropViewController {
43 | cropViewController = child
44 | child._parent = self
45 | } else if let child = child as? PhotoViewController {
46 | photoViewController = child
47 | child._parent = self
48 | }
49 | }
50 | }
51 |
52 | override var prefersStatusBarHidden : Bool {
53 | return true
54 | }
55 |
56 | // MARK: IBActions
57 |
58 | @IBAction func didPressNextButton(_ sender: AnyObject) {
59 | navigationController?.dismiss(animated: true, completion: nil)
60 | let image = topContainer.snapshot()
61 | _parent.imageView.image = image
62 | _parent = nil
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/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 |
27 |
28 |
--------------------------------------------------------------------------------
/Source/CollectionViewChangeObserver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CollectionViewChangeObserver.swift
3 | // Cropable
4 | //
5 | // Created by mac on 15/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Photos
11 |
12 | ///
13 | /// A default implementation for `PHPhotoLibraryChangeObserver`
14 | /// protocol observer object.
15 | ///
16 | open class CollectionViewChangeObserver: NSObject {
17 | open let collectionView: UICollectionView
18 |
19 | internal unowned var source: PhotoFetchable & PhotoCachable
20 |
21 | public init(collectionView: UICollectionView, source: PhotoFetchable & PhotoCachable) {
22 | self.collectionView = collectionView
23 | self.source = source
24 | }
25 | }
26 |
27 | // MARK: - PHPhotoLibraryChangeObserver
28 |
29 | extension CollectionViewChangeObserver: PHPhotoLibraryChangeObserver {
30 | open func photoLibraryDidChange(_ changeInstance: PHChange) {
31 | DispatchQueue.main.async {
32 | guard let collectionChanges = changeInstance.changeDetails(for: self.source.fetchResult as! PHFetchResult) else {
33 | return
34 | }
35 |
36 | self.source.fetchResult = collectionChanges.fetchResultAfterChanges as! PHFetchResult
37 |
38 | if !collectionChanges.hasIncrementalChanges || collectionChanges.hasMoves {
39 | self.collectionView.reloadData()
40 | } else {
41 | self.collectionView.performBatchUpdates({
42 | let removedIndexes = collectionChanges.removedIndexes
43 | if (removedIndexes?.count ?? 0) != 0 {
44 | self.collectionView.deleteItems(at: removedIndexes!.indexPaths(from: 0))
45 | }
46 |
47 | let insertedIndexes = collectionChanges.insertedIndexes
48 | if (insertedIndexes?.count ?? 0) != 0 {
49 | self.collectionView.insertItems(at: insertedIndexes!.indexPaths(from: 0))
50 | }
51 |
52 | let changedIndexes = collectionChanges.changedIndexes
53 | if (changedIndexes?.count ?? 0) != 0 {
54 | self.collectionView.reloadItems(at: changedIndexes!.indexPaths(from: 0))
55 | }
56 |
57 | }, completion: nil)
58 | }
59 |
60 | self.source.resetCachedAssets()
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Source/CropableScrollViewDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CropableScrollViewDelegate.swift
3 | // Cropable
4 | //
5 | // Created by mac on 14/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | ///
12 | /// A `UIScrollViewDelegate` for `Cropable` objects.
13 | ///
14 | open class CropableScrollViewDelegate: NSObject, UIScrollViewDelegate where T: AnyObject {
15 | fileprivate unowned var cropable: T
16 |
17 | open let linesView = LinesView()
18 |
19 | /// Indicates whether cropping should or should not be enabled for using.
20 | open var isEnabled = true {
21 | didSet {
22 | if isEnabled {
23 | cropable.cropView.isScrollEnabled = true
24 | } else {
25 | cropable.highlightArea(false, animated: false)
26 | cropable.cropView.isScrollEnabled = false
27 | }
28 | }
29 | }
30 |
31 | fileprivate var isPanning = false
32 |
33 | // MARK: Initialization
34 |
35 | public init(cropable: T) {
36 | self.cropable = cropable
37 | }
38 |
39 | // MARK: UIScrollViewDelegate
40 |
41 | open func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
42 | guard isEnabled else { return }
43 |
44 | cropable.willZoom()
45 | }
46 |
47 | open func scrollViewDidZoom(_ scrollView: UIScrollView) {
48 | guard isEnabled else { return }
49 |
50 | cropable.didZoom()
51 | }
52 |
53 | open func scrollViewDidScroll(_ scrollView: UIScrollView) {
54 | guard isEnabled else { return }
55 |
56 | if cropable.alwaysShowGuidelines {
57 | cropable.highlightArea(true)
58 | }
59 |
60 | guard isPanning else {
61 | return
62 | }
63 |
64 | cropable.highlightArea(true)
65 | }
66 |
67 | open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
68 | guard isEnabled else { return }
69 |
70 | isPanning = true
71 | cropable.highlightArea(true)
72 | }
73 |
74 | open func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
75 | guard isEnabled else { return }
76 |
77 | isPanning = false
78 |
79 | if cropable.alwaysShowGuidelines {
80 | cropable.highlightArea(true, animated: false)
81 | } else {
82 | cropable.highlightArea(false, animated: !decelerate)
83 | }
84 | }
85 |
86 | open func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
87 | guard isEnabled else { return }
88 |
89 | cropable.willEndZooming()
90 | cropable.didEndZooming()
91 | }
92 |
93 | open func viewForZooming(in scrollView: UIScrollView) -> UIView? {
94 | return cropable.childContainerView
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/CaptureViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CaptureViewController.swift
3 | // Athlee-ImagePicker
4 | //
5 | // Created by mac on 16/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 |
12 | final class CaptureViewController: UIViewController, PhotoCapturable {
13 |
14 | // MARK: Outlets
15 |
16 | @IBOutlet weak var cameraView: UIView!
17 | @IBOutlet weak var flashButton: UIButton!
18 |
19 | // MARK: Capturable properties
20 |
21 | var session: AVCaptureSession?
22 |
23 | var device: AVCaptureDevice? {
24 | didSet {
25 | guard let device = device else { return }
26 | if !device.hasFlash {
27 | flashButton.isHidden = true
28 | }
29 | }
30 | }
31 |
32 | var videoInput: AVCaptureDeviceInput?
33 | var imageOutput: AVCaptureStillImageOutput?
34 |
35 | var focusView: UIView?
36 |
37 | lazy var previewViewContainer: UIView = {
38 | return self.cameraView
39 | }()
40 |
41 | var captureNotificationObserver: CaptureNotificationObserver?
42 |
43 | // MARK: Properties
44 |
45 | var _parent: SelectionViewController!
46 |
47 | var flashMode: AVCaptureFlashMode = .on {
48 | didSet {
49 | switch flashMode {
50 | case .on:
51 | flashButton.setImage(UIImage(named: "Flash"), for: UIControlState())
52 | case .off:
53 | flashButton.setImage(UIImage(named: "FlashOff"), for: UIControlState())
54 | case .auto:
55 | flashButton.setImage(UIImage(named: "FlashAuto"), for: UIControlState())
56 | }
57 |
58 | setFlashMode(flashMode)
59 | }
60 | }
61 |
62 | // MARK: Life cycle
63 |
64 | var queue = OperationQueue()
65 |
66 | override func viewDidLoad() {
67 | super.viewDidLoad()
68 |
69 | queue.addOperation {
70 | self.prepareForCapturing()
71 | self.setFlashMode(self.flashMode)
72 | }
73 |
74 | let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(CaptureViewController.recognizedTapGesture(_:)))
75 | previewViewContainer.addGestureRecognizer(tapRecognizer)
76 | }
77 |
78 | override func viewDidLayoutSubviews() {
79 | super.viewDidLayoutSubviews()
80 | reloadPreview(previewViewContainer)
81 | }
82 |
83 | override var prefersStatusBarHidden : Bool {
84 | return true
85 | }
86 |
87 | // MARK: IBActions
88 |
89 | @IBAction func recognizedTapGesture(_ rec: UITapGestureRecognizer) {
90 | let point = rec.location(in: previewViewContainer)
91 | focus(at: point)
92 | }
93 |
94 | @IBAction func didPressCapturePhoto(_ sender: AnyObject) {
95 | captureStillImage { image in
96 | self._parent.imageView.image = image
97 | self.dismiss(animated: true, completion: nil)
98 | self._parent = nil
99 | }
100 | }
101 |
102 | @IBAction func didPressFlipButton(_ sender: AnyObject) {
103 | flipCamera()
104 | }
105 |
106 | @IBAction func didPressFlashButton(_ sender: AnyObject) {
107 | switch flashMode {
108 | case .auto:
109 | flashMode = .on
110 | case .on:
111 | flashMode = .off
112 | case .off:
113 | flashMode = .auto
114 | }
115 | }
116 |
117 | }
118 |
119 | extension CaptureViewController {
120 | func didSetFlashMode(_ flashMode: AVCaptureFlashMode) {
121 | self.flashMode = flashMode
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/Source/PhotoCapturable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoCapturable.swift
3 | // Athlee-ImagePicker
4 | //
5 | // Created by mac on 16/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Photos
11 | import AVFoundation
12 |
13 | ///
14 | /// Provides still image capturing features.
15 | ///
16 | public protocol PhotoCapturable: Capturable {
17 |
18 | /// Captures a still image from the current input.
19 | ///
20 | /// - Parameters:
21 | /// - saving: Indicates if taken image should be saved to albums.
22 | /// - handler: A handler that is called when the image is taken. Default value is `nil`.
23 | func captureStillImage(saving: Bool, handler: ((UIImage) -> Void)?)
24 |
25 | /// This function is optional. It is called when
26 | /// captured still image could not
27 | /// be saved to albums.
28 | ///
29 | /// - Parameter error: Photo Library change error.
30 | func captureStillImageFailed(with error: Error?)
31 | }
32 |
33 | // MARK: - Default implementations
34 |
35 | public extension PhotoCapturable {
36 |
37 | /// Captures a still image from the current input.
38 | ///
39 | /// - Parameters:
40 | /// - saving: Indicates if taken image should be saved to albums.
41 | /// - handler: A handler that is called when the image is taken. Default value is `nil`.
42 | func captureStillImage(saving: Bool = true, handler: ((UIImage) -> Void)?) {
43 | guard let imageOutput = imageOutput else {
44 | return
45 | }
46 |
47 | // TODO: Refactor this code
48 |
49 | DispatchQueue.global(qos: DispatchQoS.userInitiated.qosClass).async(execute: {
50 | let videoConnection = imageOutput.connection(withMediaType: AVMediaTypeVideo)
51 |
52 | imageOutput.captureStillImageAsynchronously(from: videoConnection, completionHandler: { (buffer, error) in
53 | self.session?.stopRunning()
54 |
55 | let data = AVCaptureStillImageOutput.jpegStillImageNSDataRepresentation(buffer)
56 |
57 | if let image = UIImage(data: data!) {
58 |
59 | // Image size
60 | let iw = image.size.width
61 | let ih = image.size.height
62 |
63 | // Frame size
64 | let sw = self.previewViewContainer.frame.width
65 |
66 | // The center coordinate along Y axis
67 | let rcy = ih * 0.5
68 |
69 | let imageRef = image.cgImage?.cropping(to: CGRect(x: rcy - iw * 0.5, y: 0 , width: iw, height: iw)
70 | )
71 |
72 | DispatchQueue.main.async {
73 | let resizedImage = UIImage(cgImage: imageRef!, scale: sw / iw, orientation: image.imageOrientation)
74 | handler?(resizedImage)
75 |
76 | if saving {
77 | PHPhotoLibrary.shared().performChanges({
78 | PHAssetChangeRequest.creationRequestForAsset(from: resizedImage)
79 | }, completionHandler: { [weak self] (success, error) in
80 | if !success {
81 | self?.captureStillImageFailed(with: error)
82 | }
83 | })
84 | }
85 |
86 | self.session = nil
87 | self.device = nil
88 | self.imageOutput = nil
89 | }
90 | }
91 |
92 | })
93 |
94 | })
95 | }
96 |
97 | /// This function is optional. It is called when
98 | /// captured still image could not
99 | /// be saved to albums.
100 | ///
101 | /// - Parameter error: Photo Library change error.
102 | func captureStillImageFailed(with error: Error?) { }
103 | }
104 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/ImagePickerController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // Athlee-ImagePicker
4 | //
5 | // Created by mac on 13/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class ImagePickerController: UIViewController, FloatingViewLayout {
12 |
13 | // MARK: Outlets
14 |
15 | @IBOutlet weak var floatingTopConstraint: NSLayoutConstraint!
16 | @IBOutlet weak var floatingView: UIView!
17 | @IBOutlet weak var tableView: UITableView!
18 |
19 | // MARK: FloatingViewLayout properties
20 |
21 | var animationCompletion: ((Bool) -> Void)? = nil
22 |
23 | var overlayBlurringView: UIView!
24 |
25 | var topConstraint: NSLayoutConstraint {
26 | return floatingTopConstraint
27 | }
28 |
29 | var draggingZone: DraggingZone = .all
30 |
31 | var visibleArea: CGFloat = 80
32 |
33 | var previousPoint: CGPoint?
34 |
35 | var state: State {
36 | if floatingView.frame.origin.y == 0 {
37 | return .unfolded
38 | } else if floatingView.frame.maxY == visibleArea {
39 | return .folded
40 | } else {
41 | return .moved
42 | }
43 | }
44 |
45 | var allowPanOutside = false
46 |
47 | // MARK: Life cycle
48 |
49 | override func viewDidLoad() {
50 | super.viewDidLoad()
51 |
52 | let pan = UIPanGestureRecognizer(target: self, action: #selector(ImagePickerController.didRecognizeMainPan(_:)))
53 | view.addGestureRecognizer(pan)
54 | pan.delegate = self
55 |
56 | let checkPan = UIPanGestureRecognizer(target: self, action: #selector(ImagePickerController.didRecognizeCheckPan(_:)))
57 | floatingView.addGestureRecognizer(checkPan)
58 | checkPan.delegate = self
59 | }
60 |
61 | override var prefersStatusBarHidden : Bool {
62 | return true
63 | }
64 |
65 | // MARK: IBActions
66 |
67 | @IBAction func didRecognizeMainPan(_ rec: UIPanGestureRecognizer) {
68 | receivePanGesture(recognizer: rec, with: floatingView)
69 | tableView.isScrollEnabled = true
70 | allowPanOutside = false
71 | }
72 |
73 | @IBAction func didRecognizeCheckPan(_ rec: UIPanGestureRecognizer) {
74 | allowPanOutside = true
75 | tableView.resignFirstResponder()
76 | }
77 |
78 | // MARK: FloatingViewLayout methods
79 |
80 | func prepareForMovement() {
81 | if state == .unfolded {
82 | tableView.isScrollEnabled = true
83 | } else {
84 | // TODO: Make it smoother
85 | tableView.isScrollEnabled = true
86 | }
87 | }
88 |
89 | // MARK: UIScrollViewDelegate
90 |
91 | func scrollViewDidScroll(_ scrollView: UIScrollView) {
92 | if scrollView.contentOffset.y < 0 {
93 | allowPanOutside = true
94 | } else {
95 | allowPanOutside = false
96 | }
97 | }
98 | }
99 |
100 | // MARK: - UIGestureRecognizerDelegate
101 |
102 | extension ImagePickerController: UIGestureRecognizerDelegate {
103 | func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
104 | return true
105 | }
106 |
107 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
108 | return true
109 | }
110 | }
111 |
112 | // MARK: - UITableView management
113 |
114 | extension ImagePickerController: UITableViewDataSource, UITableViewDelegate {
115 | func numberOfSections(in tableView: UITableView) -> Int {
116 | return 1
117 | }
118 |
119 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
120 | return 50
121 | }
122 |
123 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
124 | let cell = tableView.dequeueReusableCell(withIdentifier: "Cell")!
125 | return cell
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/Source/PhotoCachable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoCachable.swift
3 | // Cropable
4 | //
5 | // Created by mac on 15/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Photos
11 |
12 | ///
13 | /// Provides caching capabilities.
14 | ///
15 | public protocol PhotoCachable: class {
16 | /// A caching image manager doing all the job.
17 | var cachingImageManager: PHCachingImageManager { get set }
18 |
19 | /// A rectangle area that was previously visible.
20 | var previousPreheatRect: CGRect { get set }
21 |
22 | ///
23 | /// Resets all cached assets.
24 | ///
25 | func resetCachedAssets()
26 |
27 | ///
28 | /// Updates the assets for a given rect area and target size.
29 | ///
30 | /// - parameter rect: A given area to update assets from.
31 | /// - parameter targetSize: A visible size for assets.
32 | ///
33 | func updateCachedAssets(for rect: CGRect, targetSize: CGSize)
34 |
35 | ///
36 | /// Provides assets for a given rect area.
37 | ///
38 | /// - parameter rect: An area to get assets from.
39 | /// - returns: An array with assets found.
40 | ///
41 | func cachingAssets(at rect: CGRect) -> [PHAsset]
42 | }
43 |
44 | // MARK: - Default implementations
45 |
46 | public extension PhotoCachable {
47 | ///
48 | /// Resets all cached assets.
49 | ///
50 | func resetCachedAssets() {
51 | cachingImageManager.stopCachingImagesForAllAssets()
52 | previousPreheatRect = CGRect.zero
53 | }
54 |
55 | ///
56 | /// Updates the assets for a given rect area and target size.
57 | ///
58 | /// - parameter rect: A given area to update assets from.
59 | /// - parameter targetSize: A visible size for assets.
60 | ///
61 | func updateCachedAssets(for rect: CGRect, targetSize: CGSize) {
62 | let bounds = rect
63 | let preheatRect = bounds.insetBy(dx: 0, dy: -0.5 * bounds.height)
64 | let delta = abs(preheatRect.midY - previousPreheatRect.midY)
65 |
66 | if delta > bounds.height / 3.0 {
67 | let difference = previousPreheatRect.difference(with: preheatRect)
68 |
69 | var assetsToStartCaching: [PHAsset] = []
70 | var assetsToStopCaching: [PHAsset] = []
71 |
72 | difference.forEach { diff in
73 | if case .added(let area) = diff {
74 | assetsToStartCaching += self.cachingAssets(at: area)
75 | } else if case .removed(let area) = diff {
76 | assetsToStopCaching += self.cachingAssets(at: area)
77 | }
78 | }
79 |
80 |
81 | cachingImageManager.startCachingImages(for: assetsToStartCaching,
82 | targetSize: targetSize,
83 | contentMode: .aspectFill,
84 | options: nil)
85 |
86 | cachingImageManager.stopCachingImages(for: assetsToStopCaching,
87 | targetSize: targetSize,
88 | contentMode: .aspectFill,
89 | options: nil)
90 |
91 | previousPreheatRect = preheatRect
92 | }
93 | }
94 | }
95 |
96 | // MARK: - Util methods
97 |
98 | public extension PhotoCachable {
99 | ///
100 | /// Collects assets with provided index paths in the fetch result.
101 | ///
102 | /// - parameter indexPaths: The index paths to get indices from.
103 | /// - parameter fetchResult: Current fetch result object.
104 | /// - returns: An array with assets found.
105 | ///
106 | func assets(at indexPaths: [IndexPath], in fetchResult: PHFetchResult) -> [PHAsset] {
107 | guard indexPaths.count > 0 else {
108 | return []
109 | }
110 |
111 | let assets = indexPaths.map { fetchResult[$0.item] as! PHAsset }
112 |
113 | return assets
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/CropViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CropViewController.swift
3 | // Athlee-ImagePicker
4 | //
5 | // Created by mac on 15/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | final class CropViewController: UIViewController, FloatingViewLayout, Cropable {
12 |
13 | // MARK: Outlets
14 |
15 | @IBOutlet weak var cropContainerView: UIView!
16 |
17 | // MARK: Properties
18 |
19 | var _parent: HolderViewController!
20 |
21 | var floatingView: UIView {
22 | return _parent.topContainer
23 | }
24 |
25 | // MARK: Cropable properties
26 |
27 | var cropView = UIScrollView()
28 | var childContainerView = UIView()
29 | var childView = UIImageView()
30 | var linesView = LinesView()
31 |
32 | var topOffset: CGFloat {
33 | guard let navBar = navigationController?.navigationBar else {
34 | return 0
35 | }
36 |
37 | return !navBar.isHidden ? navBar.frame.height : 0
38 | }
39 |
40 | lazy var delegate: CropableScrollViewDelegate = {
41 | return CropableScrollViewDelegate(cropable: self)
42 | }()
43 |
44 | // MARK: FloatingViewLayout properties
45 |
46 | var animationCompletion: ((Bool) -> Void)?
47 | var overlayBlurringView: UIView!
48 |
49 | var topConstraint: NSLayoutConstraint {
50 | return _parent.topConstraint
51 | }
52 |
53 | var draggingZone: DraggingZone = .some(50)
54 |
55 | var visibleArea: CGFloat = 50
56 |
57 | var previousPoint: CGPoint?
58 |
59 | var state: State {
60 | if topConstraint.constant == 0 {
61 | return .unfolded
62 | } else if topConstraint.constant + floatingView.frame.height == visibleArea {
63 | return .folded
64 | } else {
65 | return .moved
66 | }
67 | }
68 |
69 | var allowPanOutside = false
70 |
71 | // MARK: Life cycle
72 |
73 | override func viewDidLoad() {
74 | super.viewDidLoad()
75 |
76 | addCropable(to: cropContainerView)
77 | cropView.delegate = delegate
78 | }
79 |
80 | override func viewDidLayoutSubviews() {
81 | super.viewDidLayoutSubviews()
82 | updateContent()
83 | }
84 |
85 | fileprivate var recognizersAdded = false
86 | override func viewDidAppear(_ animated: Bool) {
87 | super.viewDidAppear(animated)
88 |
89 | updateContent()
90 |
91 | if !recognizersAdded {
92 | recognizersAdded = true
93 |
94 | let pan = UIPanGestureRecognizer(target: self, action: #selector(CropViewController.didRecognizeMainPan(_:)))
95 | _parent.view.addGestureRecognizer(pan)
96 | pan.delegate = self
97 |
98 | let checkPan = UIPanGestureRecognizer(target: self, action: #selector(CropViewController.didRecognizeCheckPan(_:)))
99 | floatingView.addGestureRecognizer(checkPan)
100 | checkPan.delegate = self
101 |
102 | let tapRec = UITapGestureRecognizer(target: self, action: #selector(CropViewController.didRecognizeTap(_:)))
103 | floatingView.addGestureRecognizer(tapRec)
104 | }
105 | }
106 |
107 | override var prefersStatusBarHidden : Bool {
108 | return true
109 | }
110 |
111 | // MARK: IBActions
112 |
113 | @IBAction func didRecognizeTap(_ rec: UITapGestureRecognizer) {
114 | if state == .folded {
115 | restore(view: floatingView, to: .unfolded, animated: true)
116 | }
117 | }
118 |
119 | var zooming = false
120 | var checking = false
121 | var offset: CGFloat = 0 {
122 | didSet {
123 | if offset < 0 && state == .moved {
124 | offset = 0
125 | }
126 | }
127 | }
128 |
129 | @IBAction func didRecognizeMainPan(_ rec: UIPanGestureRecognizer) {
130 | guard !zooming else { return }
131 |
132 | if state == .unfolded {
133 | allowPanOutside = false
134 | }
135 |
136 | receivePanGesture(recognizer: rec, with: floatingView)
137 |
138 | updatePhotoCollectionViewScrolling()
139 | }
140 |
141 | @IBAction func didRecognizeCheckPan(_ rec: UIPanGestureRecognizer) {
142 | guard !zooming else { return }
143 | allowPanOutside = true
144 | }
145 |
146 | // MARK: FloatingViewLayout methods
147 |
148 | func prepareForMovement() {
149 | updateCropViewScrolling()
150 | }
151 |
152 | func didEndMoving() {
153 | updateCropViewScrolling()
154 | }
155 |
156 | func updateCropViewScrolling() {
157 | if state == .moved {
158 | delegate.isEnabled = false
159 | } else {
160 | delegate.isEnabled = true
161 | }
162 | }
163 |
164 | func updatePhotoCollectionViewScrolling() {
165 | if state == .moved {
166 | _parent.photoViewController.collectionView.contentOffset.y = offset
167 | } else {
168 | offset = _parent.photoViewController.collectionView.contentOffset.y
169 | }
170 | }
171 |
172 | // MARK: Cropable methods
173 |
174 | func willZoom() {
175 | zooming = true
176 | }
177 |
178 | func willEndZooming() {
179 | zooming = false
180 | }
181 |
182 | }
183 |
184 | extension CropViewController: UIGestureRecognizerDelegate {
185 | func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
186 | return true
187 | }
188 |
189 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
190 | return true
191 | }
192 | }
193 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/PhotoViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoViewController.swift
3 | // Athlee-ImagePicker
4 | //
5 | // Created by mac on 15/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import Photos
11 |
12 | final class PhotoViewController: UIViewController {
13 |
14 | // MARK: Outlets
15 |
16 | @IBOutlet weak var collectionView: UICollectionView!
17 |
18 | // MARK: Properties
19 |
20 | var _parent: HolderViewController!
21 |
22 | let space: CGFloat = 2
23 |
24 | lazy var fetchResult: PHFetchResult = { () -> PHFetchResult in
25 | let options = PHFetchOptions()
26 | options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
27 | let fetchResult = PHAsset.fetchAssets(with: .image, options: options)
28 |
29 | return fetchResult
30 | }()
31 |
32 | lazy var cachingImageManager = PHCachingImageManager()
33 |
34 | var previousPreheatRect: CGRect = .zero
35 |
36 | var cellSize: CGSize {
37 | let side = (collectionView.frame.width - space * 3) / 4
38 | return CGSize(
39 | width: side,
40 | height: side
41 | )
42 | }
43 |
44 | lazy var observer: CollectionViewChangeObserver = {
45 | return CollectionViewChangeObserver(collectionView: self.collectionView, source: self)
46 | }()
47 |
48 | // MARK: Life cycle
49 |
50 | override func viewDidLoad() {
51 | super.viewDidLoad()
52 |
53 | resetCachedAssets()
54 | checkPhotoAuth()
55 |
56 | if fetchResult.count > 0 {
57 | collectionView.reloadData()
58 | collectionView.selectItem(
59 | at: IndexPath(row: 0, section: 0), animated: false, scrollPosition: UICollectionViewScrollPosition())
60 | }
61 |
62 | PHPhotoLibrary.shared().register(observer)
63 |
64 | collectionView.backgroundColor = .clear
65 | }
66 |
67 | override func viewWillAppear(_ animated: Bool) {
68 | super.viewWillAppear(animated)
69 |
70 | guard let firstAsset = fetchResult.firstObject else {
71 | debugPrint("[ATHImagePickerController] Could not get the first asset!")
72 | return
73 | }
74 |
75 | cachingImageManager.requestImage(
76 | for: firstAsset,
77 | targetSize: UIScreen.main.bounds.size,
78 | contentMode: .aspectFill,
79 | options: nil) { result, info in
80 | if info!["PHImageFileURLKey"] != nil {
81 | self._parent.image = result
82 | }
83 | }
84 | }
85 |
86 | override func viewDidAppear(_ animated: Bool) {
87 | super.viewDidAppear(animated)
88 | updateCachedAssets(for: collectionView.bounds, targetSize: cellSize)
89 | }
90 |
91 |
92 | deinit {
93 | if PHPhotoLibrary.authorizationStatus() == PHAuthorizationStatus.authorized {
94 | PHPhotoLibrary.shared().unregisterChangeObserver(observer)
95 | }
96 | }
97 |
98 | }
99 |
100 | // MARK: - PhotoFetchable
101 |
102 | extension PhotoViewController: PhotoFetchable { }
103 |
104 | // MARK: - PhotoCachable
105 |
106 | extension PhotoViewController: PhotoCachable {
107 | internal func checkPhotoAuth() {
108 |
109 | PHPhotoLibrary.requestAuthorization { (status) -> Void in
110 | switch status {
111 | case .authorized:
112 | self.cachingImageManager = PHCachingImageManager()
113 | if self.fetchResult.count > 0 {
114 | // TODO: Set main initial image
115 | }
116 |
117 | case .restricted, .denied:
118 | DispatchQueue.main.async(execute: { () -> Void in
119 |
120 | // TODO: Show error
121 |
122 | })
123 | default:
124 | break
125 | }
126 | }
127 | }
128 |
129 | internal func cachingAssets(at rect: CGRect) -> [PHAsset] {
130 | let indexPaths = collectionView.indexPaths(for: rect)
131 | return assets(at: indexPaths, in: fetchResult as! PHFetchResult)
132 | }
133 | }
134 |
135 | extension PhotoViewController {
136 | func scrollViewDidScroll(_ scrollView: UIScrollView) {
137 | if scrollView == collectionView {
138 | updateCachedAssets(for: collectionView.bounds, targetSize: cellSize)
139 |
140 | if scrollView.contentOffset.y < 0 {
141 | _parent.cropViewController?.allowPanOutside = true
142 | } else {
143 | _parent.cropViewController?.allowPanOutside = false
144 | }
145 | }
146 | }
147 | }
148 |
149 | extension PhotoViewController: UICollectionViewDataSource, UICollectionViewDelegate {
150 | func numberOfSections(in collectionView: UICollectionView) -> Int {
151 | return 1
152 | }
153 |
154 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
155 | return fetchResult.count
156 | }
157 |
158 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
159 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoCell", for: indexPath) as! PhotoCell
160 |
161 | guard indexPath.item < fetchResult.count else {
162 | return cell
163 | }
164 |
165 | let asset = fetchResult[indexPath.item]
166 | cachingImageManager.requestImage(
167 | for: asset,
168 | targetSize: cellSize,
169 | contentMode: .aspectFill,
170 | options: nil) { [cell] result, info in
171 |
172 | cell.photoImageView.image = result
173 |
174 | }
175 |
176 | cell.backgroundColor = .red
177 |
178 | return cell
179 | }
180 |
181 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
182 | guard indexPath.item < fetchResult.count else {
183 | return
184 | }
185 |
186 | let asset = fetchResult[indexPath.item]
187 | cachingImageManager.requestImage(
188 | for: asset,
189 | targetSize: UIScreen.main.bounds.size,
190 | contentMode: .aspectFill,
191 | options: nil) { result, info in
192 | if info!["PHImageFileURLKey"] != nil {
193 | if let cropViewController = self._parent.cropViewController {
194 | let floatingView = cropViewController.floatingView
195 | cropViewController.restore(view: floatingView, to: .unfolded, animated: true)
196 | cropViewController.animationCompletion = { _ in
197 | self.collectionView.scrollToItem(at: indexPath, at: .top, animated: true)
198 | cropViewController.animationCompletion = nil
199 | }
200 | }
201 |
202 | self._parent.image = result
203 | }
204 | }
205 |
206 | }
207 | }
208 |
209 | extension PhotoViewController: UICollectionViewDelegateFlowLayout {
210 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
211 | return space
212 | }
213 |
214 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
215 | return space
216 | }
217 |
218 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
219 | return UIEdgeInsets.zero
220 | }
221 |
222 | func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
223 | return cellSize
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/Source/Cropable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Cropable.swift
3 | // Cropable
4 | //
5 | // Created by mac on 14/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | ///
12 | /// A protocol providing zooming features to crop the content.
13 | ///
14 | public protocol Cropable {
15 | /// A type for content view.
16 | associatedtype ChildView: UIView
17 |
18 | /// A cropable area containing the content.
19 | var cropView: UIScrollView { get set }
20 |
21 | /// A view containing the child. It is required to simplify
22 | /// any transformations on the child.
23 | var childContainerView: UIView { get set }
24 |
25 | /// A cropable content view.
26 | var childView: ChildView { get set }
27 |
28 | /// This view is shown when cropping is happening.
29 | var linesView: LinesView { get set }
30 |
31 | /// Top offset for cropable content. If your `cropView`
32 | /// is constrained with `UINavigationBar` or anything on
33 | /// the top, set this offset so the content can be properly
34 | /// centered and scaled.
35 | ///
36 | /// Default value is `0.0`.
37 | var topOffset: CGFloat { get }
38 |
39 | /// Determines whether the guidelines' grid should be
40 | /// constantly showing on the cropping view.
41 | /// Default value is `false`.
42 | var alwaysShowGuidelines: Bool { get }
43 |
44 | ///
45 | /// Adds a cropable view with its content to the provided
46 | /// container view.
47 | ///
48 | /// - parameter view: A container view.
49 | ///
50 | func addCropable(to view: UIView)
51 |
52 | ///
53 | /// Updates the current cropable content area, zoom and scale.
54 | ///
55 | func updateContent()
56 |
57 | ///
58 | /// Centers a content view in its superview depending on the size.
59 | ///
60 | /// - parameter forcing: Determines whether centering should be done forcing.
61 | /// This, generally, means that the content will be forced to get centered.
62 | ///
63 | func centerContent(forcing: Bool)
64 |
65 | ///
66 | /// This method is called whenever the zooming
67 | /// is about to start. It might be useful if
68 | /// you use a built-in `CropableScrollViewDelegate`.
69 | ///
70 | /// **ATTENTION**, default implementation
71 | /// is a placeholder!
72 | ///
73 | func willZoom()
74 |
75 | ///
76 | /// This method is called whenever the zooming
77 | /// is about to end. It might be useful if
78 | /// you use a built-in `CropableScrollViewDelegate`.
79 | ///
80 | /// **ATTENTION**, default implementation
81 | /// is a placeholder!
82 | ///
83 | func willEndZooming()
84 |
85 | ///
86 | /// Handles zoom gestures.
87 | ///
88 | func didZoom()
89 |
90 | ///
91 | /// Handles the end of zooming.
92 | ///
93 | func didEndZooming()
94 |
95 | ///
96 | /// Highlights an area of cropping by showing
97 | /// rectangular zone.
98 | ///
99 | /// - parameter highlght: A flag indicating whether it should show or hide the zone.
100 | /// - parameter animated: An animation flag, it's `true` by default.
101 | ///
102 | func highlightArea(_ highlight: Bool, animated: Bool)
103 | }
104 |
105 | // MARK: - Default implementations for UIImageView childs
106 |
107 | public extension Cropable where ChildView == UIImageView {
108 | ///
109 | /// Adds a cropable view with its content to the provided
110 | /// container view.
111 | ///
112 | /// - parameter view: A container view.
113 | ///
114 | func addCropable(to view: UIView) {
115 | cropView.translatesAutoresizingMaskIntoConstraints = false
116 | view.addSubview(cropView)
117 |
118 | let anchors = [
119 | cropView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
120 | cropView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
121 | cropView.topAnchor.constraint(equalTo: view.topAnchor),
122 | cropView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
123 | ].flatMap { $0 }
124 |
125 | NSLayoutConstraint.activate(anchors)
126 |
127 | cropView.backgroundColor = .clear
128 | cropView.showsHorizontalScrollIndicator = false
129 | cropView.showsVerticalScrollIndicator = false
130 | cropView.contentSize = view.bounds.size
131 |
132 | childContainerView.addSubview(childView)
133 | cropView.addSubview(childContainerView)
134 | }
135 |
136 | ///
137 | /// Adds a cropable view with its image to the provided
138 | /// container view.
139 | ///
140 | /// - parameter view: A container view.
141 | /// - parameter image: An image to use.
142 | ///
143 | func addCropable(to view: UIView, with image: UIImage) {
144 | addCropable(to: view)
145 | addImage(image)
146 | }
147 |
148 | ///
149 | /// Adds an image to the UIImageView child view.
150 | ///
151 | /// - parameter image: An image to use.
152 | /// - parameter adjustingContent: Indicates whether the content should be adjusted or not. Default value is `true`.
153 | ///
154 | func addImage(_ image: UIImage, adjustingContent: Bool = true) {
155 | childView.image = image
156 |
157 | if adjustingContent {
158 | childView.bounds.size = image.size
159 | childContainerView.bounds.size = image.size
160 |
161 | childContainerView.frame.origin = .zero
162 | childView.frame.origin = .zero
163 |
164 | cropView.contentOffset = .zero
165 | cropView.contentSize = image.size
166 |
167 | updateContent()
168 | highlightArea(false, animated: false)
169 | }
170 | }
171 | }
172 |
173 | // MARK: - Default implementations
174 |
175 | public extension Cropable {
176 | /// Top offset for cropable content. If your `cropView`
177 | /// is constrained with `UINavigationBar` or anything on
178 | /// the top, set this offset so the content can be properly
179 | /// centered and scaled.
180 | ///
181 | /// Default value is `0.0`.
182 | var topOffset: CGFloat {
183 | return 0
184 | }
185 |
186 | /// Determines whether the guidelines' grid should be
187 | /// constantly showing on the cropping view.
188 | /// Default value is `false`.
189 | var alwaysShowGuidelines: Bool {
190 | return false
191 | }
192 |
193 | ///
194 | /// Updates the current cropable content area, zoom and scale.
195 | ///
196 | func updateContent() {
197 | let childViewSize = childContainerView.bounds.size
198 |
199 | guard childViewSize != .zero else {
200 | debugPrint("Zero bounds found!")
201 | return
202 | }
203 |
204 | let scrollViewSize = cropView.bounds.size
205 | let widthScale = scrollViewSize.width / childViewSize.width
206 | let heightScale = scrollViewSize.height / childViewSize.height
207 |
208 | let minScale = max(scrollViewSize.width, scrollViewSize.height) / max(childViewSize.width, childViewSize.height)
209 | let scale = max(heightScale, widthScale)
210 |
211 | if let _self = self as? UIScrollViewDelegate {
212 | cropView.delegate = _self
213 | }
214 |
215 | let maxZoomScale = CGAffineTransform.scalingFactor(toFill: cropView.bounds.size,
216 | with: childView.bounds.size,
217 | atAngle: Double(0))
218 |
219 |
220 | cropView.minimumZoomScale = minScale
221 | cropView.maximumZoomScale = CGFloat(maxZoomScale)
222 | cropView.zoomScale = scale
223 |
224 | centerContent(forcing: true)
225 |
226 | highlightArea(alwaysShowGuidelines, animated: false)
227 | }
228 |
229 | ///
230 | /// Centers a content view in its superview depending on the size.
231 | ///
232 | /// - parameter forcing: Determines whether centering should be done forcing.
233 | /// This, generally, means that the content will be forced to get centered.
234 | ///
235 | func centerContent(forcing: Bool = false) {
236 | var (left, top): (CGFloat, CGFloat) = (0, 0)
237 |
238 | if cropView.contentSize.width < cropView.frame.width {
239 | left = (cropView.frame.width - cropView.contentSize.width) / 2
240 | } else if forcing {
241 | cropView.contentOffset.x = abs(cropView.frame.width - cropView.contentSize.width) / 2
242 | }
243 |
244 | if cropView.contentSize.height < cropView.frame.height {
245 | top = (cropView.frame.height - cropView.contentSize.height) / 2
246 | } else if forcing {
247 | cropView.contentOffset.y = abs(cropView.frame.height - cropView.contentSize.height) / 2
248 | }
249 |
250 | cropView.contentInset = UIEdgeInsets(top: top, left: left, bottom: top, right: left)
251 | }
252 |
253 | ///
254 | /// This method is called whenever the zooming
255 | /// is about to start. It might be useful if
256 | /// you use a built-in `CropableScrollViewDelegate`.
257 | ///
258 | /// **ATTENTION**, default implementation
259 | /// is a placeholder!
260 | ///
261 | func willZoom() { }
262 |
263 | ///
264 | /// This method is called whenever the zooming
265 | /// is about to end. It might be useful if
266 | /// you use a built-in `CropableScrollViewDelegate`.
267 | ///
268 | /// **ATTENTION**, default implementation
269 | /// is a placeholder!
270 | ///
271 | func willEndZooming() { }
272 |
273 | ///
274 | /// Handles zoom gestures.
275 | ///
276 | func didZoom() {
277 | centerContent()
278 | highlightArea(true)
279 | }
280 |
281 | ///
282 | /// Handles the end of zooming.
283 | ///
284 | func didEndZooming() {
285 | guard !alwaysShowGuidelines else { return }
286 | highlightArea(false)
287 | }
288 |
289 | ///
290 | /// Highlights an area of cropping by showing
291 | /// rectangular zone.
292 | ///
293 | /// - parameter highlght: A flag indicating whether it should show or hide the zone.
294 | /// - parameter animated: An animation flag, it's `true` by default.
295 | ///
296 | func highlightArea(_ highlight: Bool, animated: Bool = true) {
297 | guard UIApplication.shared.keyWindow != nil else {
298 | return
299 | }
300 |
301 | linesView.setNeedsDisplay()
302 | if linesView.superview == nil {
303 | cropView.insertSubview(linesView, aboveSubview: childView)
304 | linesView.backgroundColor = UIColor.clear
305 | linesView.alpha = 0
306 | } else {
307 | if animated {
308 | UIView.animate(
309 | withDuration: 0.3,
310 | delay: 0,
311 | options: [.allowUserInteraction],
312 | animations: {
313 | self.linesView.alpha = highlight ? 1 : 0
314 | },
315 |
316 | completion: nil
317 | )
318 | } else {
319 | linesView.alpha = highlight ? 1 : 0
320 | }
321 |
322 | }
323 |
324 | linesView.frame.size = CGSize(
325 | width: min(cropView.frame.width, childContainerView.frame.width),
326 | height: min(cropView.frame.height, childContainerView.frame.height)
327 | )
328 |
329 | let visibleRect = CGRect(origin: cropView.contentOffset, size: cropView.bounds.size)
330 | let intersection = visibleRect.intersection(childContainerView.frame)
331 | linesView.frame = intersection
332 | }
333 | }
334 |
--------------------------------------------------------------------------------
/Source/Capturable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Capturable.swift
3 | // Athlee-ImagePicker
4 | //
5 | // Created by mac on 16/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 |
12 | ///
13 | /// Provides featrues for previewing and capturing
14 | /// using a device camera.
15 | ///
16 | public protocol Capturable: class {
17 |
18 | /// Current capture session.
19 | var session: AVCaptureSession? { get set }
20 |
21 | /// Current capture device.
22 | var device: AVCaptureDevice? { get set }
23 |
24 | /// An input to be captured from.
25 | var videoInput: AVCaptureDeviceInput? { get set }
26 |
27 | /// An output for capturing still images.
28 | var imageOutput: AVCaptureStillImageOutput? { get set }
29 |
30 | /// A view indicating the focusing area.
31 | var focusView: UIView? { get set }
32 |
33 | /// A preview container view where the live camera record is shown.
34 | var previewViewContainer: UIView { get set }
35 |
36 | /// A default capture notification observer.
37 | /// If you want to observe by yourself,
38 | /// implement `registerForNotifications` method.
39 | /// And do not forget to unregister on deinit.
40 | var captureNotificationObserver: CaptureNotificationObserver? { get set }
41 |
42 | ///
43 | /// Provides all necessary preparations for
44 | /// the capture session.
45 | ///
46 | func prepareForCapturing()
47 |
48 | ///
49 | /// Starts capturing from a device camera.
50 | ///
51 | func startCamera()
52 |
53 | ///
54 | /// Stops capturing from a device camera.
55 | ///
56 | func stopCamera()
57 |
58 | ///
59 | /// Adds a preview layer at a given view.
60 | ///
61 | /// - parameter view: A view to contain the preview.
62 | ///
63 | func addPreviewLayer(at view: UIView)
64 |
65 | ///
66 | /// Reloads a given preview.
67 | ///
68 | /// - parameter view: A holder of the preview.
69 | ///
70 | func reloadPreview(_ view: UIView)
71 |
72 | ///
73 | /// Registers an observer for notifications (when the device
74 | /// goes in foreground).
75 | ///
76 | func registerForNotifications()
77 |
78 | ///
79 | /// Unregisters an observer for notifications.
80 | ///
81 | func unregisterForNotifications()
82 |
83 | ///
84 | /// Flips the camera.
85 | ///
86 | func flipCamera()
87 |
88 | ///
89 | /// Sets a camera capture flash mode.
90 | ///
91 | /// - parameter mode: A capture flash mode.
92 | ///
93 | func setFlashMode(_ mode: AVCaptureFlashMode)
94 |
95 | ///
96 | /// Provides camera focusing at the certain area aroung the point provided.
97 | ///
98 | /// - parameter point: A point around of which the focusing is done.
99 | ///
100 | func focus(at point: CGPoint)
101 | }
102 |
103 | // MARK: - Default implementations
104 |
105 | public extension Capturable {
106 |
107 | ///
108 | /// Provides all necessary preparations for
109 | /// the capture session.
110 | ///
111 | func prepareForCapturing() {
112 | if session == nil {
113 | session = AVCaptureSession()
114 | }
115 |
116 | for device in AVCaptureDevice.devices() {
117 | if let device = device as? AVCaptureDevice, device.position == AVCaptureDevicePosition.back {
118 | self.device = device
119 | }
120 | }
121 |
122 | do {
123 | if let session = session {
124 | videoInput = try AVCaptureDeviceInput(device: device)
125 | session.addInput(videoInput)
126 | imageOutput = AVCaptureStillImageOutput()
127 | session.addOutput(imageOutput)
128 |
129 | addPreviewLayer(at: previewViewContainer)
130 |
131 | session.startRunning()
132 | }
133 | } catch {
134 | debugPrint("Unable to connect to the device input...")
135 | }
136 |
137 | setFlashMode(.auto)
138 | startCamera()
139 | registerForNotifications()
140 | }
141 |
142 | ///
143 | /// Starts capturing from a device camera.
144 | ///
145 | func startCamera() {
146 | let status = AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo)
147 |
148 | if status == AVAuthorizationStatus.authorized {
149 | session?.startRunning()
150 | } else if status == AVAuthorizationStatus.denied || status == AVAuthorizationStatus.restricted {
151 | session?.stopRunning()
152 | }
153 | }
154 |
155 | ///
156 | /// Stops capturing from a device camera.
157 | ///
158 | func stopCamera() {
159 | session?.stopRunning()
160 | }
161 |
162 | ///
163 | /// Adds a preview layer at a given view.
164 | ///
165 | /// - parameter view: A view to contain the preview.
166 | ///
167 | func addPreviewLayer(at view: UIView) {
168 | let videoLayer = AVCaptureVideoPreviewLayer(session: session)
169 | videoLayer?.frame = view.bounds
170 | videoLayer?.videoGravity = AVLayerVideoGravityResizeAspectFill
171 |
172 | DispatchQueue.main.async {
173 | view.layer.addSublayer(videoLayer!)
174 | }
175 | }
176 |
177 | ///
178 | /// Reloads a given preview.
179 | ///
180 | /// - parameter view: A holder of the preview.
181 | ///
182 | func reloadPreview(_ view: UIView) {
183 | view.layer.sublayers?.forEach {
184 | if $0 is AVCaptureVideoPreviewLayer {
185 | $0.frame = self.previewViewContainer.bounds
186 | }
187 | }
188 | }
189 |
190 | ///
191 | /// Registers an observer for notifications (when the device
192 | /// goes in foreground).
193 | ///
194 | func registerForNotifications() {
195 | captureNotificationObserver = CaptureNotificationObserver(capturable: self)
196 | captureNotificationObserver?.register()
197 | }
198 |
199 | ///
200 | /// Unregisters an observer for notifications.
201 | ///
202 | func unregisterForNotifications() {
203 | captureNotificationObserver?.unregister()
204 | }
205 |
206 | ///
207 | /// Flips the camera.
208 | ///
209 | func flipCamera() {
210 | if !cameraIsAvailable() {
211 | return
212 | }
213 |
214 | session?.stopRunning()
215 |
216 | do {
217 | session?.beginConfiguration()
218 |
219 | if let session = session {
220 | for input in session.inputs {
221 | session.removeInput(input as! AVCaptureInput)
222 | }
223 |
224 | let position = (videoInput?.device.position == AVCaptureDevicePosition.front) ? AVCaptureDevicePosition.back : AVCaptureDevicePosition.front
225 |
226 | for device in AVCaptureDevice.devices(withMediaType: AVMediaTypeVideo) {
227 | if let device = device as? AVCaptureDevice, device.position == position {
228 | videoInput = try AVCaptureDeviceInput(device: device)
229 | session.addInput(videoInput)
230 | }
231 | }
232 | }
233 |
234 | session?.commitConfiguration()
235 | } catch {
236 | debugPrint("Unable to connect to the device input...")
237 | }
238 |
239 | session?.startRunning()
240 | }
241 |
242 | ///
243 | /// Sets a camera capture flash mode.
244 | ///
245 | /// - parameter mode: A capture flash mode.
246 | ///
247 | func setFlashMode(_ mode: AVCaptureFlashMode) {
248 | guard cameraIsAvailable() else { return }
249 |
250 | do {
251 | if let device = device {
252 | guard device.hasFlash else { return }
253 | try device.lockForConfiguration()
254 | device.flashMode = mode
255 | device.unlockForConfiguration()
256 | }
257 | } catch {
258 | debugPrint("Unable to lock device for configuration. Error: \(error)")
259 | device?.flashMode = .off
260 | return
261 | }
262 | }
263 |
264 | ///
265 | /// Provides camera focusing at the certain area aroung the point provided.
266 | ///
267 | /// - parameter point: A point around of which the focusing is done.
268 | ///
269 | func focus(at point: CGPoint) {
270 | if focusView == nil {
271 | focusView = UIView(frame: CGRect(x: 0, y: 0, width: 90, height: 90))
272 | previewViewContainer.addSubview(focusView!)
273 | }
274 |
275 | guard let superview = previewViewContainer.superview else {
276 | assertionFailure("Preview does not have a superview!")
277 | return
278 | }
279 |
280 | let viewsize = superview.bounds.size
281 | let newPoint = CGPoint(
282 | x: point.y / viewsize.height,
283 | y: 1.0 - point.x / viewsize.width
284 | )
285 |
286 | let device = AVCaptureDevice.defaultDevice(withMediaType: AVMediaTypeVideo)
287 |
288 | do {
289 | try device?.lockForConfiguration()
290 | } catch {
291 | debugPrint("Unable to lock device for configuration. Error: \(error)")
292 | return
293 | }
294 |
295 | if device?.isFocusModeSupported(AVCaptureFocusMode.autoFocus) == true {
296 | device?.focusMode = AVCaptureFocusMode.autoFocus
297 | device?.focusPointOfInterest = newPoint
298 | }
299 |
300 | if device?.isExposureModeSupported(AVCaptureExposureMode.continuousAutoExposure) == true {
301 | device?.exposureMode = AVCaptureExposureMode.continuousAutoExposure
302 | device?.exposurePointOfInterest = newPoint
303 | }
304 |
305 | device?.unlockForConfiguration()
306 |
307 | focusView?.alpha = 0.0
308 | focusView?.center = point
309 | focusView?.backgroundColor = UIColor.clear
310 | focusView?.layer.borderColor = UIColor.orange.cgColor
311 | focusView?.layer.borderWidth = 1.0
312 | focusView!.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
313 |
314 | UIView.animate(
315 | withDuration: 0.8,
316 | delay: 0.0,
317 | usingSpringWithDamping: 0.8,
318 | initialSpringVelocity: 3.0,
319 | options: UIViewAnimationOptions.curveEaseIn,
320 |
321 | animations: {
322 | self.focusView!.alpha = 1.0
323 | self.focusView!.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
324 | },
325 |
326 | completion: { _ in
327 | self.focusView!.transform = CGAffineTransform(scaleX: 1.0, y: 1.0)
328 | self.focusView!.alpha = 0
329 | }
330 | )
331 | }
332 |
333 | }
334 |
335 | // MARK: - Internal helpers
336 |
337 | internal extension Capturable {
338 | ///
339 | /// Returns `true` if a user has given access to the camera.
340 | ///
341 | func cameraIsAvailable() -> Bool {
342 | let status = AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo)
343 |
344 | if status == AVAuthorizationStatus.authorized {
345 | return true
346 | }
347 |
348 | return false
349 | }
350 |
351 | ///
352 | /// Stops and resumes current session depending on the foreground mode.
353 | ///
354 | /// - parameter notification: A foregound entry notification.
355 | ///
356 | func willEnterForegroundNotification(_ notification: Notification) {
357 | let status = AVCaptureDevice.authorizationStatus(forMediaType: AVMediaTypeVideo)
358 |
359 | if status == AVAuthorizationStatus.authorized {
360 | session?.startRunning()
361 | } else if status == AVAuthorizationStatus.denied || status == AVAuthorizationStatus.restricted {
362 | session?.stopRunning()
363 | }
364 | }
365 | }
366 |
--------------------------------------------------------------------------------
/Source/FloatingViewLayout.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FloatingViewLayout.swift
3 | // Athlee-PhotoPicker
4 | //
5 | // Created by mac on 12/07/16.
6 | // Copyright © 2016 Athlee. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | ///
12 | /// Defines states for the floating view. Originally it has 3 states:
13 | /// - Unfolded: default state, the floating view has not been moved.
14 | /// - Folded: the visible area is shown.
15 | /// - Moved: the floating view is moved.
16 | ///
17 | public enum State {
18 | case unfolded
19 | case folded
20 | case moved
21 |
22 | var description: String {
23 | switch self {
24 | case .unfolded:
25 | return "Unfolded"
26 | case .folded:
27 | return "Folded"
28 | case .moved:
29 | return "Moved"
30 | }
31 | }
32 | }
33 |
34 | ///
35 | /// Defines directions for a movement with a distance between previous
36 | /// and current touch points as an associated value. Note, that this value
37 | /// type supports only vertical states for now.
38 | ///
39 | public enum Direction {
40 | case up(delta: CGFloat)
41 | case down(delta: CGFloat)
42 | case none
43 |
44 | ///
45 | /// Constructs the same direction type with a new
46 | /// distance passed.
47 | ///
48 | /// - parameter delta: The distance passed.
49 | /// - returns: The same direction type with a new delta.
50 | ///
51 | func changed(delta: CGFloat) -> Direction {
52 | if case .up(_) = self {
53 | return .up(delta: delta)
54 | } else if case .down(_) = self {
55 | return .down(delta: delta)
56 | } else {
57 | return .none
58 | }
59 | }
60 |
61 | var description: String {
62 | switch self {
63 | case .up(_):
64 | return "Up"
65 | case .down(_):
66 | return "Down"
67 | case .none:
68 | return "None"
69 | }
70 | }
71 | }
72 |
73 | ///
74 | /// Options for floating dragging zone.
75 | ///
76 | public enum DraggingZone {
77 | case all
78 | case some(CGFloat)
79 | }
80 |
81 | ///
82 | /// The protocol allowing to move a certain view in its superview
83 | /// on pan gestures. The default implementation supports vertical
84 | /// movement of a given view staying on the top of its superview.
85 | ///
86 | public protocol FloatingViewLayout: class {
87 | /// Determines the minimum area (height or width) to be visible
88 | /// when the floating view is folded.
89 | var visibleArea: CGFloat { get set }
90 |
91 | /// A zone allowed to drag on.
92 | var draggingZone: DraggingZone { get set }
93 |
94 | /// The previous location of touch.
95 | var previousPoint: CGPoint? { get set }
96 |
97 | /// Current floating view state.
98 | var state: State { get }
99 |
100 | /// Floating view's top constraint. Note, if you use Storyboard
101 | /// your outlets will be optional, so you'll have to return unwrapped
102 | /// constraint in a `topConstraint` getter or as a computed property's value.
103 | var topConstraint: NSLayoutConstraint { get }
104 |
105 | /// Determines whether the pans should be handled being found
106 | /// outside of the floating view's frame.
107 | var allowPanOutside: Bool { get set }
108 |
109 | /// A view that fades in on moving the floating view.
110 | var overlayBlurringView: UIView! { get set }
111 |
112 | /// A completion for movement animation.
113 | var animationCompletion: ((Bool) -> Void)? { get set }
114 |
115 | /// Allows to make preparations before the movement is commited.
116 | func prepareForMovement()
117 |
118 | /// This method is called every time the movement is ended.
119 | func didEndMoving()
120 |
121 | ///
122 | /// Moves a view in a given direction with preset delta.
123 | ///
124 | /// - parameter view: The view to move.
125 | /// - parameter direction: The movement's direction with preset delta.
126 | ///
127 | func move(view: UIView, in direction: Direction)
128 |
129 | ///
130 | /// Receives a pan gesture and provides handle actions.
131 | ///
132 | /// - parameter recognizer: The sender gesture recognizer of a pan.
133 | /// - parameter view: The floating view that should be moved.
134 | ///
135 | func receivePanGesture(recognizer: UIPanGestureRecognizer, with view: UIView)
136 |
137 | ///
138 | /// Restores a given floating view to the certain state.
139 | ///
140 | /// - parameter view: The floating view.
141 | /// - parameter state: The state to change to.
142 | /// - parameter animated: Indicates whether the transition should be animated or not. Default value is `false`.
143 | ///
144 | func restore(view: UIView, to state: State, animated: Bool)
145 |
146 | ///
147 | /// Determines whether the floating view is moved enough
148 | /// to be restored when the gesture is ended.
149 | ///
150 | /// - parameter view: The floating view.
151 | /// - parameter direction: The movement's direction.
152 | ///
153 | func crossedEnough(view: UIView, in direction: Direction) -> Bool
154 | }
155 |
156 | //
157 | // MARK: - Helpers
158 | //
159 |
160 | internal extension FloatingViewLayout {
161 | func direction(withVelocity velocity: CGPoint, delta: CGFloat) -> Direction {
162 | if velocity.y < 0 {
163 | return Direction.up(delta: delta)
164 | } else if velocity.y > 0 {
165 | return Direction.down(delta: delta)
166 | } else {
167 | return Direction.none
168 | }
169 | }
170 |
171 | func closestState(of view: UIView) -> State {
172 | if view.frame.midY <= 0 {
173 | return .folded
174 | } else {
175 | return .unfolded
176 | }
177 | }
178 |
179 | func prepareOverlayBlurringViews(with view: UIView) {
180 | overlayBlurringView = UIView()
181 | overlayBlurringView.backgroundColor = .black
182 | overlayBlurringView.translatesAutoresizingMaskIntoConstraints = false
183 | overlayBlurringView.alpha = 0
184 |
185 | view.addSubview(overlayBlurringView)
186 |
187 | let anchors = [
188 | overlayBlurringView.topAnchor.constraint(equalTo: view.topAnchor),
189 | overlayBlurringView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
190 | overlayBlurringView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
191 | overlayBlurringView.leadingAnchor.constraint(equalTo: view.leadingAnchor)
192 | ].flatMap { $0 }
193 |
194 | NSLayoutConstraint.activate(anchors)
195 | }
196 | }
197 |
198 | //
199 | // MARK: - Default implmenetations
200 | //
201 |
202 | public extension FloatingViewLayout {
203 | // Default implementation. It is not required to implement this property.
204 | var animationCompletion: ((Bool) -> Void)? {
205 | return nil
206 | }
207 |
208 | // Default implementation. It is not required to implement this method.
209 | func prepareForMovement() { }
210 |
211 | // Default implementation. It is not required to implement this method.
212 | func didEndMoving() { }
213 |
214 | ///
215 | /// Restores a given floating view to the certain state.
216 | ///
217 | /// - parameter view: The floating view.
218 | /// - parameter state: The state to change to.
219 | /// - parameter animated: Indicates whether the transition should be animated or not. Default value is `false`.
220 | ///
221 | func restore(view: UIView, to state: State, animated: Bool = false) {
222 | if state == .unfolded {
223 | topConstraint.constant = 0
224 | } else if state == .folded {
225 | topConstraint.constant = -(view.frame.height - visibleArea)
226 | }
227 |
228 | if animated {
229 | if overlayBlurringView == nil {
230 | prepareOverlayBlurringViews(with: view)
231 | }
232 |
233 | UIView.animate(
234 | withDuration: 0.25,
235 | delay: 0,
236 | options: [.allowUserInteraction, .beginFromCurrentState, .curveEaseIn],
237 | animations: {
238 | view.superview?.layoutIfNeeded()
239 | self.overlayBlurringView.alpha = self.state == .unfolded ? 0 : 0.6
240 | },
241 |
242 | completion: { finished in
243 | self.didEndMoving()
244 | self.animationCompletion?(finished)
245 | }
246 | )
247 | }
248 | }
249 |
250 | ///
251 | /// Moves a view in a given direction with preset delta.
252 | ///
253 | /// - parameter view: The view to move.
254 | /// - parameter direction: The movement's direction with preset delta.
255 | ///
256 | func move(view: UIView, in direction: Direction) {
257 | switch direction {
258 | case .up(var delta):
259 | let maxY = (topConstraint.constant + view.frame.height)
260 |
261 | if maxY + delta < visibleArea {
262 | guard state == .moved else { return }
263 | delta += visibleArea - (delta + maxY)
264 | }
265 |
266 | prepareForMovement()
267 | topConstraint.constant += delta
268 | case .down(var delta):
269 | let minY = topConstraint.constant
270 |
271 | if minY + delta > 0 {
272 | guard state == .moved else { return }
273 | delta -= minY + delta
274 | }
275 |
276 | prepareForMovement()
277 | topConstraint.constant += delta
278 | case .none:
279 | print("Direction is not found yet!")
280 | }
281 |
282 |
283 | if overlayBlurringView == nil {
284 | prepareOverlayBlurringViews(with: view)
285 | }
286 |
287 | let _progress = abs(topConstraint.constant / -(view.frame.height - visibleArea))
288 | let progress = _progress > 0.6 ? 0.6 : _progress
289 | overlayBlurringView.alpha = progress
290 |
291 | didEndMoving()
292 | }
293 |
294 | ///
295 | /// Determines whether the floating view is moved enough
296 | /// to be restored when the gesture is ended.
297 | ///
298 | /// - parameter view: The floating view.
299 | /// - parameter direction: The movement's direction.
300 | ///
301 | func crossedEnough(view: UIView, in direction: Direction) -> Bool {
302 | if case .down(_) = direction {
303 | return view.frame.midY >= 0
304 | } else if case .up(_) = direction {
305 | return view.frame.midY <= 0
306 | } else {
307 | return false
308 | }
309 | }
310 |
311 | ///
312 | /// Receives a pan gesture and provides handle actions.
313 | ///
314 | /// - parameter recognizer: The sender gesture recognizer of a pan.
315 | /// - parameter view: The floating view that should be moved.
316 | ///
317 | func receivePanGesture(recognizer: UIPanGestureRecognizer, with view: UIView) {
318 | guard let superview = recognizer.view else {
319 | assertionFailure("Unable to find a registered view for UIPangestureRecognizer: \(recognizer).")
320 | return
321 | }
322 |
323 | let location = recognizer.location(in: superview)
324 | let velocity = recognizer.velocity(in: superview)
325 |
326 | if case let .some(height) = draggingZone, recognizer.state == .began {
327 | if location.y < view.frame.maxY - height {
328 | return
329 | }
330 | }
331 |
332 | guard recognizer.state != .began else {
333 | previousPoint = location
334 | return
335 | }
336 |
337 | guard let previousPoint = previousPoint else {
338 | return
339 | }
340 |
341 | let delta = (location.y - previousPoint.y)
342 | let _direction = direction(withVelocity: velocity, delta: delta)
343 | self.previousPoint = location
344 |
345 | if view.frame.contains(location) {
346 | move(view: view, in: _direction)
347 | } else {
348 | if case .down(_) = _direction, state == .moved {
349 | move(view: view, in: _direction)
350 | } else if allowPanOutside {
351 | move(view: view, in: _direction)
352 | }
353 | }
354 |
355 | if recognizer.state == .ended {
356 | self.previousPoint = nil
357 |
358 | guard state == .moved else {
359 | return
360 | }
361 |
362 | if abs(velocity.y) >= 1000.0 || crossedEnough(view: view, in: _direction) {
363 | if case .up(_) = _direction {
364 | restore(view: view, to: .folded, animated: true)
365 | } else if case .down(_) = _direction {
366 | restore(view: view, to: .unfolded, animated: true)
367 | } else {
368 | restore(view: view, to: closestState(of: view), animated: true)
369 | }
370 | } else {
371 | if case .up(_) = _direction {
372 | restore(view: view, to: .unfolded, animated: true)
373 | } else if case .down(_) = _direction {
374 | restore(view: view, to: .folded, animated: true)
375 | } else {
376 | restore(view: view, to: closestState(of: view), animated: true)
377 | }
378 | }
379 | }
380 | }
381 |
382 | }
383 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 46;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 040D62DD1D36A990003F8D8E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040D62DC1D36A990003F8D8E /* AppDelegate.swift */; };
11 | 040D62DF1D36A990003F8D8E /* ImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040D62DE1D36A990003F8D8E /* ImagePickerController.swift */; };
12 | 040D62E21D36A990003F8D8E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 040D62E01D36A990003F8D8E /* Main.storyboard */; };
13 | 040D62E41D36A990003F8D8E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 040D62E31D36A990003F8D8E /* Assets.xcassets */; };
14 | 040D62E71D36A990003F8D8E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 040D62E51D36A990003F8D8E /* LaunchScreen.storyboard */; };
15 | 040D62F21D36A991003F8D8E /* Athlee_ImagePickerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 040D62F11D36A991003F8D8E /* Athlee_ImagePickerTests.swift */; };
16 | 041D01E71D39910900D2EB81 /* CaptureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041D01E61D39910900D2EB81 /* CaptureViewController.swift */; };
17 | 0452DAE51DEB77D8009EFE32 /* Capturable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DAD51DEB77D8009EFE32 /* Capturable.swift */; };
18 | 0452DAE61DEB77D8009EFE32 /* CaptureNotificationObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DAD61DEB77D8009EFE32 /* CaptureNotificationObserver.swift */; };
19 | 0452DAE71DEB77D8009EFE32 /* CGAffineTransform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DAD71DEB77D8009EFE32 /* CGAffineTransform.swift */; };
20 | 0452DAE81DEB77D8009EFE32 /* CGRect.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DAD81DEB77D8009EFE32 /* CGRect.swift */; };
21 | 0452DAE91DEB77D8009EFE32 /* CollectionViewChangeObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DAD91DEB77D8009EFE32 /* CollectionViewChangeObserver.swift */; };
22 | 0452DAEA1DEB77D8009EFE32 /* Cropable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DADA1DEB77D8009EFE32 /* Cropable.swift */; };
23 | 0452DAEB1DEB77D8009EFE32 /* CropableScrollViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DADB1DEB77D8009EFE32 /* CropableScrollViewDelegate.swift */; };
24 | 0452DAEC1DEB77D8009EFE32 /* FloatingViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DADC1DEB77D8009EFE32 /* FloatingViewLayout.swift */; };
25 | 0452DAED1DEB77D8009EFE32 /* LinesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DADD1DEB77D8009EFE32 /* LinesView.swift */; };
26 | 0452DAEE1DEB77D8009EFE32 /* NSIndexSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DADE1DEB77D8009EFE32 /* NSIndexSet.swift */; };
27 | 0452DAEF1DEB77D8009EFE32 /* PhotoCachable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DADF1DEB77D8009EFE32 /* PhotoCachable.swift */; };
28 | 0452DAF01DEB77D8009EFE32 /* PhotoCapturable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DAE01DEB77D8009EFE32 /* PhotoCapturable.swift */; };
29 | 0452DAF11DEB77D8009EFE32 /* PhotoFetchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DAE11DEB77D8009EFE32 /* PhotoFetchable.swift */; };
30 | 0452DAF21DEB77D8009EFE32 /* Radians.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DAE21DEB77D8009EFE32 /* Radians.swift */; };
31 | 0452DAF31DEB77D8009EFE32 /* UICollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DAE31DEB77D8009EFE32 /* UICollectionView.swift */; };
32 | 0452DAF41DEB77D8009EFE32 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0452DAE41DEB77D8009EFE32 /* UIView.swift */; };
33 | 049AF44E1D38756F0097959B /* HolderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049AF44D1D38756F0097959B /* HolderViewController.swift */; };
34 | 049AF4501D3876050097959B /* CropViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049AF44F1D3876050097959B /* CropViewController.swift */; };
35 | 049AF4551D387EFC0097959B /* PhotoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049AF4541D387EFC0097959B /* PhotoCell.swift */; };
36 | 049AF4571D387F2F0097959B /* PhotoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049AF4561D387F2F0097959B /* PhotoViewController.swift */; };
37 | 049AF4591D387F840097959B /* ContainerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049AF4581D387F840097959B /* ContainerType.swift */; };
38 | 049AF45B1D38B8160097959B /* SelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 049AF45A1D38B8160097959B /* SelectionViewController.swift */; };
39 | /* End PBXBuildFile section */
40 |
41 | /* Begin PBXContainerItemProxy section */
42 | 040D62EE1D36A991003F8D8E /* PBXContainerItemProxy */ = {
43 | isa = PBXContainerItemProxy;
44 | containerPortal = 040D62D11D36A990003F8D8E /* Project object */;
45 | proxyType = 1;
46 | remoteGlobalIDString = 040D62D81D36A990003F8D8E;
47 | remoteInfo = "Athlee-ImagePicker";
48 | };
49 | /* End PBXContainerItemProxy section */
50 |
51 | /* Begin PBXFileReference section */
52 | 040D62D91D36A990003F8D8E /* Athlee-ImagePicker.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Athlee-ImagePicker.app"; sourceTree = BUILT_PRODUCTS_DIR; };
53 | 040D62DC1D36A990003F8D8E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
54 | 040D62DE1D36A990003F8D8E /* ImagePickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerController.swift; sourceTree = ""; };
55 | 040D62E11D36A990003F8D8E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
56 | 040D62E31D36A990003F8D8E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
57 | 040D62E61D36A990003F8D8E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
58 | 040D62E81D36A990003F8D8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
59 | 040D62ED1D36A991003F8D8E /* Athlee-ImagePickerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Athlee-ImagePickerTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
60 | 040D62F11D36A991003F8D8E /* Athlee_ImagePickerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Athlee_ImagePickerTests.swift; sourceTree = ""; };
61 | 040D62F31D36A991003F8D8E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
62 | 041D01E61D39910900D2EB81 /* CaptureViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaptureViewController.swift; sourceTree = ""; };
63 | 0452DAD51DEB77D8009EFE32 /* Capturable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Capturable.swift; sourceTree = ""; };
64 | 0452DAD61DEB77D8009EFE32 /* CaptureNotificationObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaptureNotificationObserver.swift; sourceTree = ""; };
65 | 0452DAD71DEB77D8009EFE32 /* CGAffineTransform.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGAffineTransform.swift; sourceTree = ""; };
66 | 0452DAD81DEB77D8009EFE32 /* CGRect.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGRect.swift; sourceTree = ""; };
67 | 0452DAD91DEB77D8009EFE32 /* CollectionViewChangeObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewChangeObserver.swift; sourceTree = ""; };
68 | 0452DADA1DEB77D8009EFE32 /* Cropable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cropable.swift; sourceTree = ""; };
69 | 0452DADB1DEB77D8009EFE32 /* CropableScrollViewDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropableScrollViewDelegate.swift; sourceTree = ""; };
70 | 0452DADC1DEB77D8009EFE32 /* FloatingViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FloatingViewLayout.swift; sourceTree = ""; };
71 | 0452DADD1DEB77D8009EFE32 /* LinesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LinesView.swift; sourceTree = ""; };
72 | 0452DADE1DEB77D8009EFE32 /* NSIndexSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NSIndexSet.swift; sourceTree = ""; };
73 | 0452DADF1DEB77D8009EFE32 /* PhotoCachable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCachable.swift; sourceTree = ""; };
74 | 0452DAE01DEB77D8009EFE32 /* PhotoCapturable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCapturable.swift; sourceTree = ""; };
75 | 0452DAE11DEB77D8009EFE32 /* PhotoFetchable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoFetchable.swift; sourceTree = ""; };
76 | 0452DAE21DEB77D8009EFE32 /* Radians.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Radians.swift; sourceTree = ""; };
77 | 0452DAE31DEB77D8009EFE32 /* UICollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UICollectionView.swift; sourceTree = ""; };
78 | 0452DAE41DEB77D8009EFE32 /* UIView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; };
79 | 049AF44D1D38756F0097959B /* HolderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HolderViewController.swift; sourceTree = ""; };
80 | 049AF44F1D3876050097959B /* CropViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CropViewController.swift; sourceTree = ""; };
81 | 049AF4541D387EFC0097959B /* PhotoCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCell.swift; sourceTree = ""; };
82 | 049AF4561D387F2F0097959B /* PhotoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoViewController.swift; sourceTree = ""; };
83 | 049AF4581D387F840097959B /* ContainerType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContainerType.swift; sourceTree = ""; };
84 | 049AF45A1D38B8160097959B /* SelectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionViewController.swift; sourceTree = ""; };
85 | /* End PBXFileReference section */
86 |
87 | /* Begin PBXFrameworksBuildPhase section */
88 | 040D62D61D36A990003F8D8E /* Frameworks */ = {
89 | isa = PBXFrameworksBuildPhase;
90 | buildActionMask = 2147483647;
91 | files = (
92 | );
93 | runOnlyForDeploymentPostprocessing = 0;
94 | };
95 | 040D62EA1D36A991003F8D8E /* Frameworks */ = {
96 | isa = PBXFrameworksBuildPhase;
97 | buildActionMask = 2147483647;
98 | files = (
99 | );
100 | runOnlyForDeploymentPostprocessing = 0;
101 | };
102 | /* End PBXFrameworksBuildPhase section */
103 |
104 | /* Begin PBXGroup section */
105 | 040D62D01D36A990003F8D8E = {
106 | isa = PBXGroup;
107 | children = (
108 | 0452DAD41DEB77D8009EFE32 /* Source */,
109 | 040D62DB1D36A990003F8D8E /* Athlee-ImagePicker */,
110 | 040D62F01D36A991003F8D8E /* Athlee-ImagePickerTests */,
111 | 040D62DA1D36A990003F8D8E /* Products */,
112 | );
113 | sourceTree = "";
114 | };
115 | 040D62DA1D36A990003F8D8E /* Products */ = {
116 | isa = PBXGroup;
117 | children = (
118 | 040D62D91D36A990003F8D8E /* Athlee-ImagePicker.app */,
119 | 040D62ED1D36A991003F8D8E /* Athlee-ImagePickerTests.xctest */,
120 | );
121 | name = Products;
122 | sourceTree = "";
123 | };
124 | 040D62DB1D36A990003F8D8E /* Athlee-ImagePicker */ = {
125 | isa = PBXGroup;
126 | children = (
127 | 040D62FD1D36A9DB003F8D8E /* Application */,
128 | 040D62FE1D36A9E4003F8D8E /* Models */,
129 | 040D62FF1D36A9E9003F8D8E /* Views */,
130 | 040D62FC1D36A99F003F8D8E /* Controllers */,
131 | 040D62DC1D36A990003F8D8E /* AppDelegate.swift */,
132 | );
133 | path = "Athlee-ImagePicker";
134 | sourceTree = "";
135 | };
136 | 040D62F01D36A991003F8D8E /* Athlee-ImagePickerTests */ = {
137 | isa = PBXGroup;
138 | children = (
139 | 040D62F11D36A991003F8D8E /* Athlee_ImagePickerTests.swift */,
140 | 040D62F31D36A991003F8D8E /* Info.plist */,
141 | );
142 | path = "Athlee-ImagePickerTests";
143 | sourceTree = "";
144 | };
145 | 040D62FC1D36A99F003F8D8E /* Controllers */ = {
146 | isa = PBXGroup;
147 | children = (
148 | 040D62DE1D36A990003F8D8E /* ImagePickerController.swift */,
149 | 049AF44D1D38756F0097959B /* HolderViewController.swift */,
150 | 049AF44F1D3876050097959B /* CropViewController.swift */,
151 | 049AF4561D387F2F0097959B /* PhotoViewController.swift */,
152 | 049AF45A1D38B8160097959B /* SelectionViewController.swift */,
153 | 041D01E61D39910900D2EB81 /* CaptureViewController.swift */,
154 | );
155 | name = Controllers;
156 | sourceTree = "";
157 | };
158 | 040D62FD1D36A9DB003F8D8E /* Application */ = {
159 | isa = PBXGroup;
160 | children = (
161 | 040D63021D36AA25003F8D8E /* Support */,
162 | 040D63011D36AA00003F8D8E /* Assets */,
163 | );
164 | name = Application;
165 | sourceTree = "";
166 | };
167 | 040D62FE1D36A9E4003F8D8E /* Models */ = {
168 | isa = PBXGroup;
169 | children = (
170 | 049AF4581D387F840097959B /* ContainerType.swift */,
171 | );
172 | name = Models;
173 | sourceTree = "";
174 | };
175 | 040D62FF1D36A9E9003F8D8E /* Views */ = {
176 | isa = PBXGroup;
177 | children = (
178 | 049AF4531D387EF60097959B /* Cells */,
179 | 040D63001D36A9F0003F8D8E /* Storyboards */,
180 | );
181 | name = Views;
182 | sourceTree = "";
183 | };
184 | 040D63001D36A9F0003F8D8E /* Storyboards */ = {
185 | isa = PBXGroup;
186 | children = (
187 | 040D62E01D36A990003F8D8E /* Main.storyboard */,
188 | 040D62E51D36A990003F8D8E /* LaunchScreen.storyboard */,
189 | );
190 | name = Storyboards;
191 | sourceTree = "";
192 | };
193 | 040D63011D36AA00003F8D8E /* Assets */ = {
194 | isa = PBXGroup;
195 | children = (
196 | 040D62E31D36A990003F8D8E /* Assets.xcassets */,
197 | );
198 | name = Assets;
199 | sourceTree = "";
200 | };
201 | 040D63021D36AA25003F8D8E /* Support */ = {
202 | isa = PBXGroup;
203 | children = (
204 | 040D62E81D36A990003F8D8E /* Info.plist */,
205 | );
206 | name = Support;
207 | sourceTree = "";
208 | };
209 | 0452DAD41DEB77D8009EFE32 /* Source */ = {
210 | isa = PBXGroup;
211 | children = (
212 | 0452DAD51DEB77D8009EFE32 /* Capturable.swift */,
213 | 0452DAD61DEB77D8009EFE32 /* CaptureNotificationObserver.swift */,
214 | 0452DAD71DEB77D8009EFE32 /* CGAffineTransform.swift */,
215 | 0452DAD81DEB77D8009EFE32 /* CGRect.swift */,
216 | 0452DAD91DEB77D8009EFE32 /* CollectionViewChangeObserver.swift */,
217 | 0452DADA1DEB77D8009EFE32 /* Cropable.swift */,
218 | 0452DADB1DEB77D8009EFE32 /* CropableScrollViewDelegate.swift */,
219 | 0452DADC1DEB77D8009EFE32 /* FloatingViewLayout.swift */,
220 | 0452DADD1DEB77D8009EFE32 /* LinesView.swift */,
221 | 0452DADE1DEB77D8009EFE32 /* NSIndexSet.swift */,
222 | 0452DADF1DEB77D8009EFE32 /* PhotoCachable.swift */,
223 | 0452DAE01DEB77D8009EFE32 /* PhotoCapturable.swift */,
224 | 0452DAE11DEB77D8009EFE32 /* PhotoFetchable.swift */,
225 | 0452DAE21DEB77D8009EFE32 /* Radians.swift */,
226 | 0452DAE31DEB77D8009EFE32 /* UICollectionView.swift */,
227 | 0452DAE41DEB77D8009EFE32 /* UIView.swift */,
228 | );
229 | name = Source;
230 | path = ../../Source;
231 | sourceTree = "";
232 | };
233 | 049AF4531D387EF60097959B /* Cells */ = {
234 | isa = PBXGroup;
235 | children = (
236 | 049AF4541D387EFC0097959B /* PhotoCell.swift */,
237 | );
238 | name = Cells;
239 | sourceTree = "";
240 | };
241 | /* End PBXGroup section */
242 |
243 | /* Begin PBXNativeTarget section */
244 | 040D62D81D36A990003F8D8E /* Athlee-ImagePicker */ = {
245 | isa = PBXNativeTarget;
246 | buildConfigurationList = 040D62F61D36A991003F8D8E /* Build configuration list for PBXNativeTarget "Athlee-ImagePicker" */;
247 | buildPhases = (
248 | 040D62D51D36A990003F8D8E /* Sources */,
249 | 040D62D61D36A990003F8D8E /* Frameworks */,
250 | 040D62D71D36A990003F8D8E /* Resources */,
251 | );
252 | buildRules = (
253 | );
254 | dependencies = (
255 | );
256 | name = "Athlee-ImagePicker";
257 | productName = "Athlee-ImagePicker";
258 | productReference = 040D62D91D36A990003F8D8E /* Athlee-ImagePicker.app */;
259 | productType = "com.apple.product-type.application";
260 | };
261 | 040D62EC1D36A991003F8D8E /* Athlee-ImagePickerTests */ = {
262 | isa = PBXNativeTarget;
263 | buildConfigurationList = 040D62F91D36A991003F8D8E /* Build configuration list for PBXNativeTarget "Athlee-ImagePickerTests" */;
264 | buildPhases = (
265 | 040D62E91D36A991003F8D8E /* Sources */,
266 | 040D62EA1D36A991003F8D8E /* Frameworks */,
267 | 040D62EB1D36A991003F8D8E /* Resources */,
268 | );
269 | buildRules = (
270 | );
271 | dependencies = (
272 | 040D62EF1D36A991003F8D8E /* PBXTargetDependency */,
273 | );
274 | name = "Athlee-ImagePickerTests";
275 | productName = "Athlee-ImagePickerTests";
276 | productReference = 040D62ED1D36A991003F8D8E /* Athlee-ImagePickerTests.xctest */;
277 | productType = "com.apple.product-type.bundle.unit-test";
278 | };
279 | /* End PBXNativeTarget section */
280 |
281 | /* Begin PBXProject section */
282 | 040D62D11D36A990003F8D8E /* Project object */ = {
283 | isa = PBXProject;
284 | attributes = {
285 | LastSwiftUpdateCheck = 0730;
286 | LastUpgradeCheck = 0810;
287 | ORGANIZATIONNAME = Athlee;
288 | TargetAttributes = {
289 | 040D62D81D36A990003F8D8E = {
290 | CreatedOnToolsVersion = 7.3.1;
291 | DevelopmentTeam = RG4N372RZE;
292 | LastSwiftMigration = 0810;
293 | };
294 | 040D62EC1D36A991003F8D8E = {
295 | CreatedOnToolsVersion = 7.3.1;
296 | LastSwiftMigration = 0810;
297 | TestTargetID = 040D62D81D36A990003F8D8E;
298 | };
299 | };
300 | };
301 | buildConfigurationList = 040D62D41D36A990003F8D8E /* Build configuration list for PBXProject "Athlee-ImagePicker" */;
302 | compatibilityVersion = "Xcode 3.2";
303 | developmentRegion = English;
304 | hasScannedForEncodings = 0;
305 | knownRegions = (
306 | en,
307 | Base,
308 | );
309 | mainGroup = 040D62D01D36A990003F8D8E;
310 | productRefGroup = 040D62DA1D36A990003F8D8E /* Products */;
311 | projectDirPath = "";
312 | projectRoot = "";
313 | targets = (
314 | 040D62D81D36A990003F8D8E /* Athlee-ImagePicker */,
315 | 040D62EC1D36A991003F8D8E /* Athlee-ImagePickerTests */,
316 | );
317 | };
318 | /* End PBXProject section */
319 |
320 | /* Begin PBXResourcesBuildPhase section */
321 | 040D62D71D36A990003F8D8E /* Resources */ = {
322 | isa = PBXResourcesBuildPhase;
323 | buildActionMask = 2147483647;
324 | files = (
325 | 040D62E71D36A990003F8D8E /* LaunchScreen.storyboard in Resources */,
326 | 040D62E41D36A990003F8D8E /* Assets.xcassets in Resources */,
327 | 040D62E21D36A990003F8D8E /* Main.storyboard in Resources */,
328 | );
329 | runOnlyForDeploymentPostprocessing = 0;
330 | };
331 | 040D62EB1D36A991003F8D8E /* Resources */ = {
332 | isa = PBXResourcesBuildPhase;
333 | buildActionMask = 2147483647;
334 | files = (
335 | );
336 | runOnlyForDeploymentPostprocessing = 0;
337 | };
338 | /* End PBXResourcesBuildPhase section */
339 |
340 | /* Begin PBXSourcesBuildPhase section */
341 | 040D62D51D36A990003F8D8E /* Sources */ = {
342 | isa = PBXSourcesBuildPhase;
343 | buildActionMask = 2147483647;
344 | files = (
345 | 0452DAED1DEB77D8009EFE32 /* LinesView.swift in Sources */,
346 | 0452DAEB1DEB77D8009EFE32 /* CropableScrollViewDelegate.swift in Sources */,
347 | 0452DAE81DEB77D8009EFE32 /* CGRect.swift in Sources */,
348 | 0452DAEE1DEB77D8009EFE32 /* NSIndexSet.swift in Sources */,
349 | 049AF4551D387EFC0097959B /* PhotoCell.swift in Sources */,
350 | 040D62DF1D36A990003F8D8E /* ImagePickerController.swift in Sources */,
351 | 0452DAF31DEB77D8009EFE32 /* UICollectionView.swift in Sources */,
352 | 049AF4591D387F840097959B /* ContainerType.swift in Sources */,
353 | 0452DAEF1DEB77D8009EFE32 /* PhotoCachable.swift in Sources */,
354 | 0452DAE61DEB77D8009EFE32 /* CaptureNotificationObserver.swift in Sources */,
355 | 0452DAF11DEB77D8009EFE32 /* PhotoFetchable.swift in Sources */,
356 | 041D01E71D39910900D2EB81 /* CaptureViewController.swift in Sources */,
357 | 0452DAE51DEB77D8009EFE32 /* Capturable.swift in Sources */,
358 | 049AF4501D3876050097959B /* CropViewController.swift in Sources */,
359 | 0452DAE91DEB77D8009EFE32 /* CollectionViewChangeObserver.swift in Sources */,
360 | 049AF45B1D38B8160097959B /* SelectionViewController.swift in Sources */,
361 | 0452DAF01DEB77D8009EFE32 /* PhotoCapturable.swift in Sources */,
362 | 049AF44E1D38756F0097959B /* HolderViewController.swift in Sources */,
363 | 0452DAE71DEB77D8009EFE32 /* CGAffineTransform.swift in Sources */,
364 | 0452DAF21DEB77D8009EFE32 /* Radians.swift in Sources */,
365 | 0452DAF41DEB77D8009EFE32 /* UIView.swift in Sources */,
366 | 0452DAEC1DEB77D8009EFE32 /* FloatingViewLayout.swift in Sources */,
367 | 049AF4571D387F2F0097959B /* PhotoViewController.swift in Sources */,
368 | 0452DAEA1DEB77D8009EFE32 /* Cropable.swift in Sources */,
369 | 040D62DD1D36A990003F8D8E /* AppDelegate.swift in Sources */,
370 | );
371 | runOnlyForDeploymentPostprocessing = 0;
372 | };
373 | 040D62E91D36A991003F8D8E /* Sources */ = {
374 | isa = PBXSourcesBuildPhase;
375 | buildActionMask = 2147483647;
376 | files = (
377 | 040D62F21D36A991003F8D8E /* Athlee_ImagePickerTests.swift in Sources */,
378 | );
379 | runOnlyForDeploymentPostprocessing = 0;
380 | };
381 | /* End PBXSourcesBuildPhase section */
382 |
383 | /* Begin PBXTargetDependency section */
384 | 040D62EF1D36A991003F8D8E /* PBXTargetDependency */ = {
385 | isa = PBXTargetDependency;
386 | target = 040D62D81D36A990003F8D8E /* Athlee-ImagePicker */;
387 | targetProxy = 040D62EE1D36A991003F8D8E /* PBXContainerItemProxy */;
388 | };
389 | /* End PBXTargetDependency section */
390 |
391 | /* Begin PBXVariantGroup section */
392 | 040D62E01D36A990003F8D8E /* Main.storyboard */ = {
393 | isa = PBXVariantGroup;
394 | children = (
395 | 040D62E11D36A990003F8D8E /* Base */,
396 | );
397 | name = Main.storyboard;
398 | sourceTree = "";
399 | };
400 | 040D62E51D36A990003F8D8E /* LaunchScreen.storyboard */ = {
401 | isa = PBXVariantGroup;
402 | children = (
403 | 040D62E61D36A990003F8D8E /* Base */,
404 | );
405 | name = LaunchScreen.storyboard;
406 | sourceTree = "";
407 | };
408 | /* End PBXVariantGroup section */
409 |
410 | /* Begin XCBuildConfiguration section */
411 | 040D62F41D36A991003F8D8E /* Debug */ = {
412 | isa = XCBuildConfiguration;
413 | buildSettings = {
414 | ALWAYS_SEARCH_USER_PATHS = NO;
415 | CLANG_ANALYZER_NONNULL = YES;
416 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
417 | CLANG_CXX_LIBRARY = "libc++";
418 | CLANG_ENABLE_MODULES = YES;
419 | CLANG_ENABLE_OBJC_ARC = YES;
420 | CLANG_WARN_BOOL_CONVERSION = YES;
421 | CLANG_WARN_CONSTANT_CONVERSION = YES;
422 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
423 | CLANG_WARN_EMPTY_BODY = YES;
424 | CLANG_WARN_ENUM_CONVERSION = YES;
425 | CLANG_WARN_INFINITE_RECURSION = YES;
426 | CLANG_WARN_INT_CONVERSION = YES;
427 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
428 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
429 | CLANG_WARN_UNREACHABLE_CODE = YES;
430 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
431 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
432 | COPY_PHASE_STRIP = NO;
433 | DEBUG_INFORMATION_FORMAT = dwarf;
434 | ENABLE_STRICT_OBJC_MSGSEND = YES;
435 | ENABLE_TESTABILITY = YES;
436 | GCC_C_LANGUAGE_STANDARD = gnu99;
437 | GCC_DYNAMIC_NO_PIC = NO;
438 | GCC_NO_COMMON_BLOCKS = YES;
439 | GCC_OPTIMIZATION_LEVEL = 0;
440 | GCC_PREPROCESSOR_DEFINITIONS = (
441 | "DEBUG=1",
442 | "$(inherited)",
443 | );
444 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
445 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
446 | GCC_WARN_UNDECLARED_SELECTOR = YES;
447 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
448 | GCC_WARN_UNUSED_FUNCTION = YES;
449 | GCC_WARN_UNUSED_VARIABLE = YES;
450 | IPHONEOS_DEPLOYMENT_TARGET = 9.3;
451 | MTL_ENABLE_DEBUG_INFO = YES;
452 | ONLY_ACTIVE_ARCH = YES;
453 | SDKROOT = iphoneos;
454 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
455 | };
456 | name = Debug;
457 | };
458 | 040D62F51D36A991003F8D8E /* Release */ = {
459 | isa = XCBuildConfiguration;
460 | buildSettings = {
461 | ALWAYS_SEARCH_USER_PATHS = NO;
462 | CLANG_ANALYZER_NONNULL = YES;
463 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
464 | CLANG_CXX_LIBRARY = "libc++";
465 | CLANG_ENABLE_MODULES = YES;
466 | CLANG_ENABLE_OBJC_ARC = YES;
467 | CLANG_WARN_BOOL_CONVERSION = YES;
468 | CLANG_WARN_CONSTANT_CONVERSION = YES;
469 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
470 | CLANG_WARN_EMPTY_BODY = YES;
471 | CLANG_WARN_ENUM_CONVERSION = YES;
472 | CLANG_WARN_INFINITE_RECURSION = YES;
473 | CLANG_WARN_INT_CONVERSION = YES;
474 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
475 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
476 | CLANG_WARN_UNREACHABLE_CODE = YES;
477 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
478 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
479 | COPY_PHASE_STRIP = NO;
480 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
481 | ENABLE_NS_ASSERTIONS = NO;
482 | ENABLE_STRICT_OBJC_MSGSEND = YES;
483 | GCC_C_LANGUAGE_STANDARD = gnu99;
484 | GCC_NO_COMMON_BLOCKS = YES;
485 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
486 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
487 | GCC_WARN_UNDECLARED_SELECTOR = YES;
488 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
489 | GCC_WARN_UNUSED_FUNCTION = YES;
490 | GCC_WARN_UNUSED_VARIABLE = YES;
491 | IPHONEOS_DEPLOYMENT_TARGET = 9.3;
492 | MTL_ENABLE_DEBUG_INFO = NO;
493 | SDKROOT = iphoneos;
494 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
495 | VALIDATE_PRODUCT = YES;
496 | };
497 | name = Release;
498 | };
499 | 040D62F71D36A991003F8D8E /* Debug */ = {
500 | isa = XCBuildConfiguration;
501 | buildSettings = {
502 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
503 | DEVELOPMENT_TEAM = RG4N372RZE;
504 | INFOPLIST_FILE = "Athlee-ImagePicker/Info.plist";
505 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
506 | PRODUCT_BUNDLE_IDENTIFIER = "io.athlee.Athlee-ImagePicker-2";
507 | PRODUCT_NAME = "$(TARGET_NAME)";
508 | SWIFT_VERSION = 3.0;
509 | };
510 | name = Debug;
511 | };
512 | 040D62F81D36A991003F8D8E /* Release */ = {
513 | isa = XCBuildConfiguration;
514 | buildSettings = {
515 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
516 | DEVELOPMENT_TEAM = RG4N372RZE;
517 | INFOPLIST_FILE = "Athlee-ImagePicker/Info.plist";
518 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
519 | PRODUCT_BUNDLE_IDENTIFIER = "io.athlee.Athlee-ImagePicker-2";
520 | PRODUCT_NAME = "$(TARGET_NAME)";
521 | SWIFT_VERSION = 3.0;
522 | };
523 | name = Release;
524 | };
525 | 040D62FA1D36A991003F8D8E /* Debug */ = {
526 | isa = XCBuildConfiguration;
527 | buildSettings = {
528 | BUNDLE_LOADER = "$(TEST_HOST)";
529 | INFOPLIST_FILE = "Athlee-ImagePickerTests/Info.plist";
530 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
531 | PRODUCT_BUNDLE_IDENTIFIER = "io.athlee.Athlee-ImagePickerTests";
532 | PRODUCT_NAME = "$(TARGET_NAME)";
533 | SWIFT_VERSION = 3.0;
534 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Athlee-ImagePicker.app/Athlee-ImagePicker";
535 | };
536 | name = Debug;
537 | };
538 | 040D62FB1D36A991003F8D8E /* Release */ = {
539 | isa = XCBuildConfiguration;
540 | buildSettings = {
541 | BUNDLE_LOADER = "$(TEST_HOST)";
542 | INFOPLIST_FILE = "Athlee-ImagePickerTests/Info.plist";
543 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
544 | PRODUCT_BUNDLE_IDENTIFIER = "io.athlee.Athlee-ImagePickerTests";
545 | PRODUCT_NAME = "$(TARGET_NAME)";
546 | SWIFT_VERSION = 3.0;
547 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Athlee-ImagePicker.app/Athlee-ImagePicker";
548 | };
549 | name = Release;
550 | };
551 | /* End XCBuildConfiguration section */
552 |
553 | /* Begin XCConfigurationList section */
554 | 040D62D41D36A990003F8D8E /* Build configuration list for PBXProject "Athlee-ImagePicker" */ = {
555 | isa = XCConfigurationList;
556 | buildConfigurations = (
557 | 040D62F41D36A991003F8D8E /* Debug */,
558 | 040D62F51D36A991003F8D8E /* Release */,
559 | );
560 | defaultConfigurationIsVisible = 0;
561 | defaultConfigurationName = Release;
562 | };
563 | 040D62F61D36A991003F8D8E /* Build configuration list for PBXNativeTarget "Athlee-ImagePicker" */ = {
564 | isa = XCConfigurationList;
565 | buildConfigurations = (
566 | 040D62F71D36A991003F8D8E /* Debug */,
567 | 040D62F81D36A991003F8D8E /* Release */,
568 | );
569 | defaultConfigurationIsVisible = 0;
570 | defaultConfigurationName = Release;
571 | };
572 | 040D62F91D36A991003F8D8E /* Build configuration list for PBXNativeTarget "Athlee-ImagePickerTests" */ = {
573 | isa = XCConfigurationList;
574 | buildConfigurations = (
575 | 040D62FA1D36A991003F8D8E /* Debug */,
576 | 040D62FB1D36A991003F8D8E /* Release */,
577 | );
578 | defaultConfigurationIsVisible = 0;
579 | defaultConfigurationName = Release;
580 | };
581 | /* End XCConfigurationList section */
582 | };
583 | rootObject = 040D62D11D36A990003F8D8E /* Project object */;
584 | }
585 |
--------------------------------------------------------------------------------
/Example/Athlee-ImagePicker/Athlee-ImagePicker/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 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
41 |
52 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
281 |
282 |
283 |
284 |
285 |
286 |
287 |
288 |
289 |
290 |
291 |
292 |
293 |
294 |
295 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 |
337 |
338 |
345 |
352 |
353 |
354 |
355 |
356 |
357 |
358 |
359 |
360 |
361 |
362 |
363 |
364 |
365 |
366 |
367 |
368 |
369 |
370 |
371 |
372 |
373 |
374 |
375 |
376 |
377 |
378 |
379 |
380 |
381 |
382 |
383 |
384 |
385 |
386 |
387 |
388 |
389 |
390 |
391 |
392 |
393 |
394 |
395 |
396 |
397 |
398 |
399 |
400 |
401 |
--------------------------------------------------------------------------------