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