├── Example ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── Info.plist └── ViewController.swift ├── ImagePickerTrayController.xcodeproj ├── project.pbxproj └── project.xcworkspace │ └── contents.xcworkspacedata ├── ImagePickerTrayController ├── AnimationController.swift ├── Cells │ ├── ActionCell.swift │ ├── CameraCell.swift │ ├── CameraOverlayView.swift │ └── ImageCell.swift ├── Helpers │ ├── UIEdgeInsetsExtensions.swift │ └── UIImageExtensions.swift ├── ImagePickerAction.swift ├── ImagePickerTrayController.h ├── ImagePickerTrayController.swift ├── Images.xcassets │ ├── ActionCell-Chevron.imageset │ │ ├── ActionCell-Chevron@2x.png │ │ ├── ActionCell-Chevron@3x.png │ │ └── Contents.json │ ├── CameraOverlayView-CameraFlip.imageset │ │ ├── CameraOverlayView-CameraFlip@2x.png │ │ ├── CameraOverlayView-CameraFlip@3x.png │ │ └── Contents.json │ ├── Contents.json │ ├── ImageCell-Cloud.imageset │ │ ├── Contents.json │ │ ├── ImageCell-Cloud@2x.png │ │ └── ImageCell-Cloud@3x.png │ ├── ImageCell-Selected.imageset │ │ ├── Contents.json │ │ ├── ImageCell-Selected@2x.png │ │ └── ImageCell-Selected@3x.png │ ├── ImageCell-Shadow.imageset │ │ ├── Contents.json │ │ ├── ImageCell-Shadow@2x.png │ │ └── ImageCell-Shadow@3x.png │ ├── ImageCell-Unselected.imageset │ │ ├── Contents.json │ │ ├── ImageCell-Unselected@2x.png │ │ └── ImageCell-Unselected@3x.png │ ├── ImageCell-Video.imageset │ │ ├── Contents.json │ │ ├── ImageCell-Video@2x.png │ │ └── ImageCell-Video@3x.png │ ├── ImagePickerAction-Camera.imageset │ │ ├── Contents.json │ │ ├── ImagePickerAction-Camera@2x.png │ │ └── ImagePickerAction-Camera@3x.png │ └── ImagePickerAction-Library.imageset │ │ ├── Contents.json │ │ ├── ImagePickerAction-Library@2x.png │ │ └── ImagePickerAction-Library@3x.png ├── Info.plist └── TransitionController.swift ├── ImagePickerTrayControllerTests ├── ImagePickerTrayControllerTests.swift └── Info.plist ├── LICENSE ├── README.md └── Screenshots └── Example.png /Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Laurin Brandner on 14.10.16. 6 | // Copyright © 2016 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? = { 15 | let window = UIWindow(frame: UIScreen.main.bounds) 16 | window.backgroundColor = .white 17 | 18 | return window 19 | }() 20 | 21 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { 22 | window?.rootViewController = UINavigationController(rootViewController: ViewController()) 23 | window?.makeKeyAndVisible() 24 | 25 | return true 26 | } 27 | 28 | } 29 | 30 | -------------------------------------------------------------------------------- /Example/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 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /Example/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | IPTC 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | NSCameraUsageDescription 26 | To take dem pictures 27 | NSPhotoLibraryUsageDescription 28 | This is just a demo you silly :) 29 | UILaunchStoryboardName 30 | LaunchScreen 31 | UIRequiredDeviceCapabilities 32 | 33 | armv7 34 | 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by Laurin Brandner on 14.10.16. 6 | // Copyright © 2016 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import ImagePickerTrayController 11 | 12 | class ViewController: UIViewController { 13 | 14 | var rows: [Int] { 15 | return (0..<100).map { $0 } 16 | } 17 | 18 | fileprivate lazy var tableView: UITableView = { 19 | let tableView = UITableView() 20 | tableView.dataSource = self 21 | tableView.translatesAutoresizingMaskIntoConstraints = false 22 | 23 | return tableView 24 | }() 25 | 26 | // MARK: - View Lifecycle 27 | 28 | override func loadView() { 29 | super.loadView() 30 | 31 | view.addSubview(tableView) 32 | tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true 33 | tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true 34 | tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true 35 | tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true 36 | } 37 | 38 | override func viewDidLoad() { 39 | super.viewDidLoad() 40 | 41 | navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Images", style: .plain, target: self, action: #selector(toggleImagePickerTray(_:))) 42 | 43 | let cellClass = UITableViewCell.self 44 | tableView.register(cellClass, forCellReuseIdentifier: NSStringFromClass(cellClass)) 45 | 46 | let center = NotificationCenter.default 47 | center.addObserver(self, selector: #selector(willShowImagePickerTray(notification:)), name: ImagePickerTrayWillShow, object: nil) 48 | center.addObserver(self, selector: #selector(willHideImagePickerTray(notification:)), name: ImagePickerTrayWillHide, object: nil) 49 | } 50 | 51 | // MARK: - 52 | 53 | @objc fileprivate func toggleImagePickerTray(_: UIBarButtonItem) { 54 | if presentedViewController != nil { 55 | hideImagePickerTray() 56 | } 57 | else { 58 | showImagePickerTray() 59 | } 60 | } 61 | 62 | fileprivate func showImagePickerTray() { 63 | let controller = ImagePickerTrayController() 64 | controller.add(action: .cameraAction { _ in 65 | print("Show Camera") 66 | }) 67 | controller.add(action: .libraryAction { _ in 68 | print("Show Library") 69 | }) 70 | present(controller, animated: true, completion: nil) 71 | } 72 | 73 | fileprivate func hideImagePickerTray() { 74 | dismiss(animated: true, completion: nil) 75 | } 76 | 77 | @objc fileprivate func willShowImagePickerTray(notification: Notification) { 78 | guard let userInfo = notification.userInfo, 79 | let frame = userInfo[ImagePickerTrayFrameUserInfoKey] as? CGRect else { 80 | return 81 | } 82 | 83 | let duration: TimeInterval = (userInfo[ImagePickerTrayAnimationDurationUserInfoKey] as? TimeInterval) ?? 0 84 | animateContentInset(inset: frame.height, duration: duration, curve: UIViewAnimationCurve(rawValue: 0)!) 85 | } 86 | 87 | @objc fileprivate func willHideImagePickerTray(notification: Notification) { 88 | guard let userInfo = notification.userInfo else { 89 | return 90 | } 91 | 92 | let duration: TimeInterval = (userInfo[ImagePickerTrayAnimationDurationUserInfoKey] as? TimeInterval) ?? 0 93 | animateContentInset(inset: 0, duration: duration, curve: UIViewAnimationCurve(rawValue: 0)!) 94 | } 95 | 96 | fileprivate func animateContentInset(inset bottomInset: CGFloat, duration: TimeInterval, curve: UIViewAnimationCurve) { 97 | var inset = tableView.contentInset 98 | inset.bottom = bottomInset 99 | 100 | var offset = tableView.contentOffset 101 | offset.y = max(0, offset.y - bottomInset) 102 | 103 | let options = UIViewAnimationOptions(rawValue: UInt(curve.rawValue) << 16) 104 | UIView.animate(withDuration: duration, delay: 0, options: options, animations: { 105 | self.tableView.contentInset = inset 106 | self.tableView.contentOffset = offset 107 | self.tableView.scrollIndicatorInsets = inset 108 | }, completion: nil) 109 | } 110 | 111 | } 112 | 113 | // MARK: - UITableViewDataSource 114 | 115 | extension ViewController: UITableViewDataSource { 116 | 117 | func numberOfSections(in tableView: UITableView) -> Int { 118 | return 1 119 | } 120 | 121 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 122 | return rows.count 123 | } 124 | 125 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 126 | let cell = tableView.dequeueReusableCell(withIdentifier: NSStringFromClass(UITableViewCell.self), for: indexPath) 127 | cell.textLabel?.text = String(rows[indexPath.row]) 128 | 129 | return cell 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /ImagePickerTrayController.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4909255A1DE4E600001027EC /* ActionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490925571DE4E600001027EC /* ActionCell.swift */; }; 11 | 4909255B1DE4E600001027EC /* CameraCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490925581DE4E600001027EC /* CameraCell.swift */; }; 12 | 4909255C1DE4E600001027EC /* ImageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 490925591DE4E600001027EC /* ImageCell.swift */; }; 13 | 4909255E1DE4F089001027EC /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4909255D1DE4F089001027EC /* Images.xcassets */; }; 14 | 491D5EB41DE451CD007EC01A /* ImagePickerAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D5EB31DE451CD007EC01A /* ImagePickerAction.swift */; }; 15 | 491E96B51EA2A62900603E56 /* AnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491E96B41EA2A62900603E56 /* AnimationController.swift */; }; 16 | 492715FA1DE7835200A9DD5B /* UIImageExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 492715F91DE7835200A9DD5B /* UIImageExtensions.swift */; }; 17 | 4958FF381DEA33F7000E1201 /* CameraOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4958FF371DEA33F7000E1201 /* CameraOverlayView.swift */; }; 18 | 4970E5261DB0DD400078ED23 /* ImagePickerTrayControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4970E5251DB0DD400078ED23 /* ImagePickerTrayControllerTests.swift */; }; 19 | 4970E56D1DB0DE110078ED23 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4970E56C1DB0DE110078ED23 /* AppDelegate.swift */; }; 20 | 4970E56F1DB0DE110078ED23 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4970E56E1DB0DE110078ED23 /* ViewController.swift */; }; 21 | 4970E5741DB0DE110078ED23 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4970E5731DB0DE110078ED23 /* Assets.xcassets */; }; 22 | 4970E5771DB0DE110078ED23 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4970E5751DB0DE110078ED23 /* LaunchScreen.storyboard */; }; 23 | 4970E58C1DB0E0410078ED23 /* ImagePickerTrayController.h in Headers */ = {isa = PBXBuildFile; fileRef = 4970E58A1DB0E0410078ED23 /* ImagePickerTrayController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 24 | 4970E58F1DB0E0410078ED23 /* ImagePickerTrayController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4970E5881DB0E0410078ED23 /* ImagePickerTrayController.framework */; }; 25 | 4970E5901DB0E0410078ED23 /* ImagePickerTrayController.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4970E5881DB0E0410078ED23 /* ImagePickerTrayController.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 26 | 4970E5961DB0E0550078ED23 /* ImagePickerTrayController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4970E5951DB0E0550078ED23 /* ImagePickerTrayController.swift */; }; 27 | 49A9DB6C1EA4B84B008DD8FB /* TransitionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49A9DB6B1EA4B84B008DD8FB /* TransitionController.swift */; }; 28 | 49FC9BD11DEB472900AC1AA8 /* UIEdgeInsetsExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49FC9BD01DEB472900AC1AA8 /* UIEdgeInsetsExtensions.swift */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXContainerItemProxy section */ 32 | 4970E58D1DB0E0410078ED23 /* PBXContainerItemProxy */ = { 33 | isa = PBXContainerItemProxy; 34 | containerPortal = 4970E5051DB0DD400078ED23 /* Project object */; 35 | proxyType = 1; 36 | remoteGlobalIDString = 4970E5871DB0E0410078ED23; 37 | remoteInfo = ImagePickerTrayController; 38 | }; 39 | /* End PBXContainerItemProxy section */ 40 | 41 | /* Begin PBXCopyFilesBuildPhase section */ 42 | 4970E5941DB0E0410078ED23 /* Embed Frameworks */ = { 43 | isa = PBXCopyFilesBuildPhase; 44 | buildActionMask = 2147483647; 45 | dstPath = ""; 46 | dstSubfolderSpec = 10; 47 | files = ( 48 | 4970E5901DB0E0410078ED23 /* ImagePickerTrayController.framework in Embed Frameworks */, 49 | ); 50 | name = "Embed Frameworks"; 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXCopyFilesBuildPhase section */ 54 | 55 | /* Begin PBXFileReference section */ 56 | 490925571DE4E600001027EC /* ActionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionCell.swift; sourceTree = ""; }; 57 | 490925581DE4E600001027EC /* CameraCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraCell.swift; sourceTree = ""; }; 58 | 490925591DE4E600001027EC /* ImageCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageCell.swift; sourceTree = ""; }; 59 | 4909255D1DE4F089001027EC /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 60 | 491D5EB31DE451CD007EC01A /* ImagePickerAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerAction.swift; sourceTree = ""; }; 61 | 491E96B41EA2A62900603E56 /* AnimationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationController.swift; sourceTree = ""; }; 62 | 492715F91DE7835200A9DD5B /* UIImageExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImageExtensions.swift; sourceTree = ""; }; 63 | 4958FF371DEA33F7000E1201 /* CameraOverlayView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraOverlayView.swift; sourceTree = ""; }; 64 | 4970E5211DB0DD400078ED23 /* ImagePickerTrayControllerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ImagePickerTrayControllerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 4970E5251DB0DD400078ED23 /* ImagePickerTrayControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerTrayControllerTests.swift; sourceTree = ""; }; 66 | 4970E5271DB0DD400078ED23 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 67 | 4970E56A1DB0DE110078ED23 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 68 | 4970E56C1DB0DE110078ED23 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 69 | 4970E56E1DB0DE110078ED23 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 70 | 4970E5731DB0DE110078ED23 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 71 | 4970E5761DB0DE110078ED23 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 72 | 4970E5781DB0DE110078ED23 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 73 | 4970E5881DB0E0410078ED23 /* ImagePickerTrayController.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ImagePickerTrayController.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74 | 4970E58A1DB0E0410078ED23 /* ImagePickerTrayController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ImagePickerTrayController.h; sourceTree = ""; }; 75 | 4970E58B1DB0E0410078ED23 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 76 | 4970E5951DB0E0550078ED23 /* ImagePickerTrayController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerTrayController.swift; sourceTree = ""; }; 77 | 49A9DB6B1EA4B84B008DD8FB /* TransitionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionController.swift; sourceTree = ""; }; 78 | 49FC9BD01DEB472900AC1AA8 /* UIEdgeInsetsExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIEdgeInsetsExtensions.swift; sourceTree = ""; }; 79 | /* End PBXFileReference section */ 80 | 81 | /* Begin PBXFrameworksBuildPhase section */ 82 | 4970E51E1DB0DD400078ED23 /* Frameworks */ = { 83 | isa = PBXFrameworksBuildPhase; 84 | buildActionMask = 2147483647; 85 | files = ( 86 | ); 87 | runOnlyForDeploymentPostprocessing = 0; 88 | }; 89 | 4970E5671DB0DE110078ED23 /* Frameworks */ = { 90 | isa = PBXFrameworksBuildPhase; 91 | buildActionMask = 2147483647; 92 | files = ( 93 | 4970E58F1DB0E0410078ED23 /* ImagePickerTrayController.framework in Frameworks */, 94 | ); 95 | runOnlyForDeploymentPostprocessing = 0; 96 | }; 97 | 4970E5841DB0E0410078ED23 /* Frameworks */ = { 98 | isa = PBXFrameworksBuildPhase; 99 | buildActionMask = 2147483647; 100 | files = ( 101 | ); 102 | runOnlyForDeploymentPostprocessing = 0; 103 | }; 104 | /* End PBXFrameworksBuildPhase section */ 105 | 106 | /* Begin PBXGroup section */ 107 | 490925561DE4E600001027EC /* Cells */ = { 108 | isa = PBXGroup; 109 | children = ( 110 | 490925571DE4E600001027EC /* ActionCell.swift */, 111 | 490925581DE4E600001027EC /* CameraCell.swift */, 112 | 4958FF371DEA33F7000E1201 /* CameraOverlayView.swift */, 113 | 490925591DE4E600001027EC /* ImageCell.swift */, 114 | ); 115 | path = Cells; 116 | sourceTree = ""; 117 | }; 118 | 492715F81DE7835200A9DD5B /* Helpers */ = { 119 | isa = PBXGroup; 120 | children = ( 121 | 492715F91DE7835200A9DD5B /* UIImageExtensions.swift */, 122 | 49FC9BD01DEB472900AC1AA8 /* UIEdgeInsetsExtensions.swift */, 123 | ); 124 | path = Helpers; 125 | sourceTree = ""; 126 | }; 127 | 4970E5041DB0DD400078ED23 = { 128 | isa = PBXGroup; 129 | children = ( 130 | 4970E5891DB0E0410078ED23 /* ImagePickerTrayController */, 131 | 4970E5241DB0DD400078ED23 /* ImagePickerTrayControllerTests */, 132 | 4970E56B1DB0DE110078ED23 /* Example */, 133 | 4970E50E1DB0DD400078ED23 /* Products */, 134 | ); 135 | sourceTree = ""; 136 | }; 137 | 4970E50E1DB0DD400078ED23 /* Products */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | 4970E5211DB0DD400078ED23 /* ImagePickerTrayControllerTests.xctest */, 141 | 4970E56A1DB0DE110078ED23 /* Example.app */, 142 | 4970E5881DB0E0410078ED23 /* ImagePickerTrayController.framework */, 143 | ); 144 | name = Products; 145 | sourceTree = ""; 146 | }; 147 | 4970E5241DB0DD400078ED23 /* ImagePickerTrayControllerTests */ = { 148 | isa = PBXGroup; 149 | children = ( 150 | 4970E5251DB0DD400078ED23 /* ImagePickerTrayControllerTests.swift */, 151 | 4970E5271DB0DD400078ED23 /* Info.plist */, 152 | ); 153 | path = ImagePickerTrayControllerTests; 154 | sourceTree = ""; 155 | }; 156 | 4970E56B1DB0DE110078ED23 /* Example */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | 4970E56C1DB0DE110078ED23 /* AppDelegate.swift */, 160 | 4970E56E1DB0DE110078ED23 /* ViewController.swift */, 161 | 4970E57C1DB0DE210078ED23 /* Supporting Files */, 162 | ); 163 | path = Example; 164 | sourceTree = ""; 165 | }; 166 | 4970E57C1DB0DE210078ED23 /* Supporting Files */ = { 167 | isa = PBXGroup; 168 | children = ( 169 | 4970E5731DB0DE110078ED23 /* Assets.xcassets */, 170 | 4970E5751DB0DE110078ED23 /* LaunchScreen.storyboard */, 171 | 4970E5781DB0DE110078ED23 /* Info.plist */, 172 | ); 173 | name = "Supporting Files"; 174 | sourceTree = ""; 175 | }; 176 | 4970E5891DB0E0410078ED23 /* ImagePickerTrayController */ = { 177 | isa = PBXGroup; 178 | children = ( 179 | 491E96B41EA2A62900603E56 /* AnimationController.swift */, 180 | 49A9DB6B1EA4B84B008DD8FB /* TransitionController.swift */, 181 | 491D5EB31DE451CD007EC01A /* ImagePickerAction.swift */, 182 | 4970E5951DB0E0550078ED23 /* ImagePickerTrayController.swift */, 183 | 490925561DE4E600001027EC /* Cells */, 184 | 492715F81DE7835200A9DD5B /* Helpers */, 185 | 4909255D1DE4F089001027EC /* Images.xcassets */, 186 | 4970E5971DB0E0620078ED23 /* Supporting Files */, 187 | ); 188 | path = ImagePickerTrayController; 189 | sourceTree = ""; 190 | }; 191 | 4970E5971DB0E0620078ED23 /* Supporting Files */ = { 192 | isa = PBXGroup; 193 | children = ( 194 | 4970E58A1DB0E0410078ED23 /* ImagePickerTrayController.h */, 195 | 4970E58B1DB0E0410078ED23 /* Info.plist */, 196 | ); 197 | name = "Supporting Files"; 198 | sourceTree = ""; 199 | }; 200 | /* End PBXGroup section */ 201 | 202 | /* Begin PBXHeadersBuildPhase section */ 203 | 4970E5851DB0E0410078ED23 /* Headers */ = { 204 | isa = PBXHeadersBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | 4970E58C1DB0E0410078ED23 /* ImagePickerTrayController.h in Headers */, 208 | ); 209 | runOnlyForDeploymentPostprocessing = 0; 210 | }; 211 | /* End PBXHeadersBuildPhase section */ 212 | 213 | /* Begin PBXNativeTarget section */ 214 | 4970E5201DB0DD400078ED23 /* ImagePickerTrayControllerTests */ = { 215 | isa = PBXNativeTarget; 216 | buildConfigurationList = 4970E52D1DB0DD400078ED23 /* Build configuration list for PBXNativeTarget "ImagePickerTrayControllerTests" */; 217 | buildPhases = ( 218 | 4970E51D1DB0DD400078ED23 /* Sources */, 219 | 4970E51E1DB0DD400078ED23 /* Frameworks */, 220 | 4970E51F1DB0DD400078ED23 /* Resources */, 221 | ); 222 | buildRules = ( 223 | ); 224 | dependencies = ( 225 | ); 226 | name = ImagePickerTrayControllerTests; 227 | productName = ImagePickerTrayControllerTests; 228 | productReference = 4970E5211DB0DD400078ED23 /* ImagePickerTrayControllerTests.xctest */; 229 | productType = "com.apple.product-type.bundle.unit-test"; 230 | }; 231 | 4970E5691DB0DE110078ED23 /* Example */ = { 232 | isa = PBXNativeTarget; 233 | buildConfigurationList = 4970E5791DB0DE110078ED23 /* Build configuration list for PBXNativeTarget "Example" */; 234 | buildPhases = ( 235 | 4970E5661DB0DE110078ED23 /* Sources */, 236 | 4970E5671DB0DE110078ED23 /* Frameworks */, 237 | 4970E5681DB0DE110078ED23 /* Resources */, 238 | 4970E5941DB0E0410078ED23 /* Embed Frameworks */, 239 | ); 240 | buildRules = ( 241 | ); 242 | dependencies = ( 243 | 4970E58E1DB0E0410078ED23 /* PBXTargetDependency */, 244 | ); 245 | name = Example; 246 | productName = Example; 247 | productReference = 4970E56A1DB0DE110078ED23 /* Example.app */; 248 | productType = "com.apple.product-type.application"; 249 | }; 250 | 4970E5871DB0E0410078ED23 /* ImagePickerTrayController */ = { 251 | isa = PBXNativeTarget; 252 | buildConfigurationList = 4970E5911DB0E0410078ED23 /* Build configuration list for PBXNativeTarget "ImagePickerTrayController" */; 253 | buildPhases = ( 254 | 4970E5831DB0E0410078ED23 /* Sources */, 255 | 4970E5841DB0E0410078ED23 /* Frameworks */, 256 | 4970E5851DB0E0410078ED23 /* Headers */, 257 | 4970E5861DB0E0410078ED23 /* Resources */, 258 | ); 259 | buildRules = ( 260 | ); 261 | dependencies = ( 262 | ); 263 | name = ImagePickerTrayController; 264 | productName = ImagePickerTrayController; 265 | productReference = 4970E5881DB0E0410078ED23 /* ImagePickerTrayController.framework */; 266 | productType = "com.apple.product-type.framework"; 267 | }; 268 | /* End PBXNativeTarget section */ 269 | 270 | /* Begin PBXProject section */ 271 | 4970E5051DB0DD400078ED23 /* Project object */ = { 272 | isa = PBXProject; 273 | attributes = { 274 | LastSwiftUpdateCheck = 0800; 275 | LastUpgradeCheck = 0810; 276 | ORGANIZATIONNAME = "Laurin Brandner"; 277 | TargetAttributes = { 278 | 4970E5201DB0DD400078ED23 = { 279 | CreatedOnToolsVersion = 8.0; 280 | ProvisioningStyle = Automatic; 281 | TestTargetID = 4970E50C1DB0DD400078ED23; 282 | }; 283 | 4970E5691DB0DE110078ED23 = { 284 | CreatedOnToolsVersion = 8.0; 285 | DevelopmentTeam = 5953RHWYWT; 286 | ProvisioningStyle = Automatic; 287 | }; 288 | 4970E5871DB0E0410078ED23 = { 289 | CreatedOnToolsVersion = 8.0; 290 | LastSwiftMigration = 0800; 291 | ProvisioningStyle = Automatic; 292 | }; 293 | }; 294 | }; 295 | buildConfigurationList = 4970E5081DB0DD400078ED23 /* Build configuration list for PBXProject "ImagePickerTrayController" */; 296 | compatibilityVersion = "Xcode 3.2"; 297 | developmentRegion = English; 298 | hasScannedForEncodings = 0; 299 | knownRegions = ( 300 | en, 301 | Base, 302 | ); 303 | mainGroup = 4970E5041DB0DD400078ED23; 304 | productRefGroup = 4970E50E1DB0DD400078ED23 /* Products */; 305 | projectDirPath = ""; 306 | projectRoot = ""; 307 | targets = ( 308 | 4970E5871DB0E0410078ED23 /* ImagePickerTrayController */, 309 | 4970E5201DB0DD400078ED23 /* ImagePickerTrayControllerTests */, 310 | 4970E5691DB0DE110078ED23 /* Example */, 311 | ); 312 | }; 313 | /* End PBXProject section */ 314 | 315 | /* Begin PBXResourcesBuildPhase section */ 316 | 4970E51F1DB0DD400078ED23 /* Resources */ = { 317 | isa = PBXResourcesBuildPhase; 318 | buildActionMask = 2147483647; 319 | files = ( 320 | ); 321 | runOnlyForDeploymentPostprocessing = 0; 322 | }; 323 | 4970E5681DB0DE110078ED23 /* Resources */ = { 324 | isa = PBXResourcesBuildPhase; 325 | buildActionMask = 2147483647; 326 | files = ( 327 | 4970E5771DB0DE110078ED23 /* LaunchScreen.storyboard in Resources */, 328 | 4970E5741DB0DE110078ED23 /* Assets.xcassets in Resources */, 329 | ); 330 | runOnlyForDeploymentPostprocessing = 0; 331 | }; 332 | 4970E5861DB0E0410078ED23 /* Resources */ = { 333 | isa = PBXResourcesBuildPhase; 334 | buildActionMask = 2147483647; 335 | files = ( 336 | 4909255E1DE4F089001027EC /* Images.xcassets in Resources */, 337 | ); 338 | runOnlyForDeploymentPostprocessing = 0; 339 | }; 340 | /* End PBXResourcesBuildPhase section */ 341 | 342 | /* Begin PBXSourcesBuildPhase section */ 343 | 4970E51D1DB0DD400078ED23 /* Sources */ = { 344 | isa = PBXSourcesBuildPhase; 345 | buildActionMask = 2147483647; 346 | files = ( 347 | 4970E5261DB0DD400078ED23 /* ImagePickerTrayControllerTests.swift in Sources */, 348 | ); 349 | runOnlyForDeploymentPostprocessing = 0; 350 | }; 351 | 4970E5661DB0DE110078ED23 /* Sources */ = { 352 | isa = PBXSourcesBuildPhase; 353 | buildActionMask = 2147483647; 354 | files = ( 355 | 4970E56F1DB0DE110078ED23 /* ViewController.swift in Sources */, 356 | 4970E56D1DB0DE110078ED23 /* AppDelegate.swift in Sources */, 357 | ); 358 | runOnlyForDeploymentPostprocessing = 0; 359 | }; 360 | 4970E5831DB0E0410078ED23 /* Sources */ = { 361 | isa = PBXSourcesBuildPhase; 362 | buildActionMask = 2147483647; 363 | files = ( 364 | 4958FF381DEA33F7000E1201 /* CameraOverlayView.swift in Sources */, 365 | 4909255A1DE4E600001027EC /* ActionCell.swift in Sources */, 366 | 492715FA1DE7835200A9DD5B /* UIImageExtensions.swift in Sources */, 367 | 491D5EB41DE451CD007EC01A /* ImagePickerAction.swift in Sources */, 368 | 49A9DB6C1EA4B84B008DD8FB /* TransitionController.swift in Sources */, 369 | 4970E5961DB0E0550078ED23 /* ImagePickerTrayController.swift in Sources */, 370 | 4909255B1DE4E600001027EC /* CameraCell.swift in Sources */, 371 | 4909255C1DE4E600001027EC /* ImageCell.swift in Sources */, 372 | 491E96B51EA2A62900603E56 /* AnimationController.swift in Sources */, 373 | 49FC9BD11DEB472900AC1AA8 /* UIEdgeInsetsExtensions.swift in Sources */, 374 | ); 375 | runOnlyForDeploymentPostprocessing = 0; 376 | }; 377 | /* End PBXSourcesBuildPhase section */ 378 | 379 | /* Begin PBXTargetDependency section */ 380 | 4970E58E1DB0E0410078ED23 /* PBXTargetDependency */ = { 381 | isa = PBXTargetDependency; 382 | target = 4970E5871DB0E0410078ED23 /* ImagePickerTrayController */; 383 | targetProxy = 4970E58D1DB0E0410078ED23 /* PBXContainerItemProxy */; 384 | }; 385 | /* End PBXTargetDependency section */ 386 | 387 | /* Begin PBXVariantGroup section */ 388 | 4970E5751DB0DE110078ED23 /* LaunchScreen.storyboard */ = { 389 | isa = PBXVariantGroup; 390 | children = ( 391 | 4970E5761DB0DE110078ED23 /* Base */, 392 | ); 393 | name = LaunchScreen.storyboard; 394 | sourceTree = ""; 395 | }; 396 | /* End PBXVariantGroup section */ 397 | 398 | /* Begin XCBuildConfiguration section */ 399 | 4970E5281DB0DD400078ED23 /* Debug */ = { 400 | isa = XCBuildConfiguration; 401 | buildSettings = { 402 | ALWAYS_SEARCH_USER_PATHS = NO; 403 | CLANG_ANALYZER_NONNULL = YES; 404 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 405 | CLANG_CXX_LIBRARY = "libc++"; 406 | CLANG_ENABLE_MODULES = YES; 407 | CLANG_ENABLE_OBJC_ARC = YES; 408 | CLANG_WARN_BOOL_CONVERSION = YES; 409 | CLANG_WARN_CONSTANT_CONVERSION = YES; 410 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 411 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 412 | CLANG_WARN_EMPTY_BODY = YES; 413 | CLANG_WARN_ENUM_CONVERSION = YES; 414 | CLANG_WARN_INFINITE_RECURSION = YES; 415 | CLANG_WARN_INT_CONVERSION = YES; 416 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 417 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 418 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 419 | CLANG_WARN_UNREACHABLE_CODE = YES; 420 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 421 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 422 | COPY_PHASE_STRIP = NO; 423 | DEBUG_INFORMATION_FORMAT = dwarf; 424 | ENABLE_STRICT_OBJC_MSGSEND = YES; 425 | ENABLE_TESTABILITY = YES; 426 | GCC_C_LANGUAGE_STANDARD = gnu99; 427 | GCC_DYNAMIC_NO_PIC = NO; 428 | GCC_NO_COMMON_BLOCKS = YES; 429 | GCC_OPTIMIZATION_LEVEL = 0; 430 | GCC_PREPROCESSOR_DEFINITIONS = ( 431 | "DEBUG=1", 432 | "$(inherited)", 433 | ); 434 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 435 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 436 | GCC_WARN_UNDECLARED_SELECTOR = YES; 437 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 438 | GCC_WARN_UNUSED_FUNCTION = YES; 439 | GCC_WARN_UNUSED_VARIABLE = YES; 440 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 441 | MTL_ENABLE_DEBUG_INFO = YES; 442 | ONLY_ACTIVE_ARCH = YES; 443 | SDKROOT = iphoneos; 444 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 445 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 446 | TARGETED_DEVICE_FAMILY = "1,2"; 447 | }; 448 | name = Debug; 449 | }; 450 | 4970E5291DB0DD400078ED23 /* Release */ = { 451 | isa = XCBuildConfiguration; 452 | buildSettings = { 453 | ALWAYS_SEARCH_USER_PATHS = NO; 454 | CLANG_ANALYZER_NONNULL = YES; 455 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 456 | CLANG_CXX_LIBRARY = "libc++"; 457 | CLANG_ENABLE_MODULES = YES; 458 | CLANG_ENABLE_OBJC_ARC = YES; 459 | CLANG_WARN_BOOL_CONVERSION = YES; 460 | CLANG_WARN_CONSTANT_CONVERSION = YES; 461 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 462 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 463 | CLANG_WARN_EMPTY_BODY = YES; 464 | CLANG_WARN_ENUM_CONVERSION = YES; 465 | CLANG_WARN_INFINITE_RECURSION = YES; 466 | CLANG_WARN_INT_CONVERSION = YES; 467 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 468 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 469 | CLANG_WARN_SUSPICIOUS_MOVES = YES; 470 | CLANG_WARN_UNREACHABLE_CODE = YES; 471 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 472 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 473 | COPY_PHASE_STRIP = NO; 474 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 475 | ENABLE_NS_ASSERTIONS = NO; 476 | ENABLE_STRICT_OBJC_MSGSEND = YES; 477 | GCC_C_LANGUAGE_STANDARD = gnu99; 478 | GCC_NO_COMMON_BLOCKS = YES; 479 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 480 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 481 | GCC_WARN_UNDECLARED_SELECTOR = YES; 482 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 483 | GCC_WARN_UNUSED_FUNCTION = YES; 484 | GCC_WARN_UNUSED_VARIABLE = YES; 485 | IPHONEOS_DEPLOYMENT_TARGET = 10.0; 486 | MTL_ENABLE_DEBUG_INFO = NO; 487 | SDKROOT = iphoneos; 488 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 489 | TARGETED_DEVICE_FAMILY = "1,2"; 490 | VALIDATE_PRODUCT = YES; 491 | }; 492 | name = Release; 493 | }; 494 | 4970E52E1DB0DD400078ED23 /* Debug */ = { 495 | isa = XCBuildConfiguration; 496 | buildSettings = { 497 | BUNDLE_LOADER = "$(TEST_HOST)"; 498 | INFOPLIST_FILE = ImagePickerTrayControllerTests/Info.plist; 499 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 500 | PRODUCT_BUNDLE_IDENTIFIER = ch.laurinbrandner.ImagePickerTrayControllerTests; 501 | PRODUCT_NAME = "$(TARGET_NAME)"; 502 | SWIFT_VERSION = 3.0; 503 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ImagePickerTrayController.app/ImagePickerTrayController"; 504 | }; 505 | name = Debug; 506 | }; 507 | 4970E52F1DB0DD400078ED23 /* Release */ = { 508 | isa = XCBuildConfiguration; 509 | buildSettings = { 510 | BUNDLE_LOADER = "$(TEST_HOST)"; 511 | INFOPLIST_FILE = ImagePickerTrayControllerTests/Info.plist; 512 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 513 | PRODUCT_BUNDLE_IDENTIFIER = ch.laurinbrandner.ImagePickerTrayControllerTests; 514 | PRODUCT_NAME = "$(TARGET_NAME)"; 515 | SWIFT_VERSION = 3.0; 516 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ImagePickerTrayController.app/ImagePickerTrayController"; 517 | }; 518 | name = Release; 519 | }; 520 | 4970E57A1DB0DE110078ED23 /* Debug */ = { 521 | isa = XCBuildConfiguration; 522 | buildSettings = { 523 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 524 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 525 | DEVELOPMENT_TEAM = 5953RHWYWT; 526 | INFOPLIST_FILE = Example/Info.plist; 527 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 528 | PRODUCT_BUNDLE_IDENTIFIER = ch.laurinbrandner.Example; 529 | PRODUCT_NAME = "$(TARGET_NAME)"; 530 | SWIFT_VERSION = 3.0; 531 | }; 532 | name = Debug; 533 | }; 534 | 4970E57B1DB0DE110078ED23 /* Release */ = { 535 | isa = XCBuildConfiguration; 536 | buildSettings = { 537 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 538 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 539 | DEVELOPMENT_TEAM = 5953RHWYWT; 540 | INFOPLIST_FILE = Example/Info.plist; 541 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 542 | PRODUCT_BUNDLE_IDENTIFIER = ch.laurinbrandner.Example; 543 | PRODUCT_NAME = "$(TARGET_NAME)"; 544 | SWIFT_VERSION = 3.0; 545 | }; 546 | name = Release; 547 | }; 548 | 4970E5921DB0E0410078ED23 /* Debug */ = { 549 | isa = XCBuildConfiguration; 550 | buildSettings = { 551 | CLANG_ENABLE_MODULES = YES; 552 | CODE_SIGN_IDENTITY = ""; 553 | CURRENT_PROJECT_VERSION = 1; 554 | DEFINES_MODULE = YES; 555 | DYLIB_COMPATIBILITY_VERSION = 1; 556 | DYLIB_CURRENT_VERSION = 1; 557 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 558 | INFOPLIST_FILE = ImagePickerTrayController/Info.plist; 559 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 560 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 561 | PRODUCT_BUNDLE_IDENTIFIER = ch.laurinbrandner.ImagePickerTrayController; 562 | PRODUCT_NAME = "$(TARGET_NAME)"; 563 | SKIP_INSTALL = YES; 564 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 565 | SWIFT_VERSION = 3.0; 566 | VERSIONING_SYSTEM = "apple-generic"; 567 | VERSION_INFO_PREFIX = ""; 568 | }; 569 | name = Debug; 570 | }; 571 | 4970E5931DB0E0410078ED23 /* Release */ = { 572 | isa = XCBuildConfiguration; 573 | buildSettings = { 574 | CLANG_ENABLE_MODULES = YES; 575 | CODE_SIGN_IDENTITY = ""; 576 | CURRENT_PROJECT_VERSION = 1; 577 | DEFINES_MODULE = YES; 578 | DYLIB_COMPATIBILITY_VERSION = 1; 579 | DYLIB_CURRENT_VERSION = 1; 580 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 581 | INFOPLIST_FILE = ImagePickerTrayController/Info.plist; 582 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 583 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 584 | PRODUCT_BUNDLE_IDENTIFIER = ch.laurinbrandner.ImagePickerTrayController; 585 | PRODUCT_NAME = "$(TARGET_NAME)"; 586 | SKIP_INSTALL = YES; 587 | SWIFT_VERSION = 3.0; 588 | VERSIONING_SYSTEM = "apple-generic"; 589 | VERSION_INFO_PREFIX = ""; 590 | }; 591 | name = Release; 592 | }; 593 | /* End XCBuildConfiguration section */ 594 | 595 | /* Begin XCConfigurationList section */ 596 | 4970E5081DB0DD400078ED23 /* Build configuration list for PBXProject "ImagePickerTrayController" */ = { 597 | isa = XCConfigurationList; 598 | buildConfigurations = ( 599 | 4970E5281DB0DD400078ED23 /* Debug */, 600 | 4970E5291DB0DD400078ED23 /* Release */, 601 | ); 602 | defaultConfigurationIsVisible = 0; 603 | defaultConfigurationName = Release; 604 | }; 605 | 4970E52D1DB0DD400078ED23 /* Build configuration list for PBXNativeTarget "ImagePickerTrayControllerTests" */ = { 606 | isa = XCConfigurationList; 607 | buildConfigurations = ( 608 | 4970E52E1DB0DD400078ED23 /* Debug */, 609 | 4970E52F1DB0DD400078ED23 /* Release */, 610 | ); 611 | defaultConfigurationIsVisible = 0; 612 | defaultConfigurationName = Release; 613 | }; 614 | 4970E5791DB0DE110078ED23 /* Build configuration list for PBXNativeTarget "Example" */ = { 615 | isa = XCConfigurationList; 616 | buildConfigurations = ( 617 | 4970E57A1DB0DE110078ED23 /* Debug */, 618 | 4970E57B1DB0DE110078ED23 /* Release */, 619 | ); 620 | defaultConfigurationIsVisible = 0; 621 | defaultConfigurationName = Release; 622 | }; 623 | 4970E5911DB0E0410078ED23 /* Build configuration list for PBXNativeTarget "ImagePickerTrayController" */ = { 624 | isa = XCConfigurationList; 625 | buildConfigurations = ( 626 | 4970E5921DB0E0410078ED23 /* Debug */, 627 | 4970E5931DB0E0410078ED23 /* Release */, 628 | ); 629 | defaultConfigurationIsVisible = 0; 630 | defaultConfigurationName = Release; 631 | }; 632 | /* End XCConfigurationList section */ 633 | }; 634 | rootObject = 4970E5051DB0DD400078ED23 /* Project object */; 635 | } 636 | -------------------------------------------------------------------------------- /ImagePickerTrayController.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ImagePickerTrayController/AnimationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimationController.swift 3 | // ImagePickerTrayController 4 | // 5 | // Created by Laurin Brandner on 15.04.17. 6 | // Copyright © 2017 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class AnimationController: NSObject { 12 | 13 | enum Transition { 14 | case presentation(UIPanGestureRecognizer) 15 | case dismissal 16 | } 17 | 18 | fileprivate let transition: Transition 19 | 20 | init(transition: Transition) { 21 | self.transition = transition 22 | super.init() 23 | } 24 | 25 | } 26 | 27 | extension AnimationController: UIViewControllerAnimatedTransitioning { 28 | 29 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 30 | return 0.25 31 | } 32 | 33 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 34 | switch transition { 35 | case .presentation(let gestureRecognizer): 36 | present(with: gestureRecognizer, using: transitionContext) 37 | case .dismissal: 38 | dismiss(using: transitionContext) 39 | } 40 | } 41 | 42 | private func present(with gestureRecognizer: UIPanGestureRecognizer, using transitionContext: UIViewControllerContextTransitioning) { 43 | guard let to = transitionContext.viewController(forKey: .to) as? ImagePickerTrayController else { 44 | transitionContext.completeTransition(false) 45 | return 46 | } 47 | 48 | let container = transitionContext.containerView 49 | container.window?.addGestureRecognizer(gestureRecognizer) 50 | 51 | container.addSubview(to.view) 52 | container.frame = CGRect(x: 0, y: container.bounds.height-to.trayHeight, width: container.bounds.width, height: to.trayHeight) 53 | to.view.transform = CGAffineTransform(translationX: 0, y: to.trayHeight) 54 | 55 | let duration = transitionDuration(using: transitionContext) 56 | UIView.animate(withDuration: duration, delay: 0, options: .allowUserInteraction, animations: { 57 | to.view.transform = .identity 58 | }, completion: { _ in 59 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 60 | }) 61 | } 62 | 63 | private func dismiss(using transitionContext: UIViewControllerContextTransitioning) { 64 | guard let from = transitionContext.viewController(forKey: .from) as? ImagePickerTrayController else { 65 | transitionContext.completeTransition(false) 66 | return 67 | } 68 | 69 | let duration = transitionDuration(using: transitionContext) 70 | UIView.animate(withDuration: duration, delay: 0, options: .allowUserInteraction, animations: { 71 | from.view.frame.origin.y += from.trayHeight 72 | }, completion: { _ in 73 | if !transitionContext.transitionWasCancelled { 74 | // from.view.removeFromSuperview() 75 | } 76 | transitionContext.completeTransition(!transitionContext.transitionWasCancelled) 77 | }) 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /ImagePickerTrayController/Cells/ActionCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionCell.swift 3 | // ImagePickerTrayController 4 | // 5 | // Created by Laurin Brandner on 22.11.16. 6 | // Copyright © 2016 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | let spacing = CGPoint(x: 26, y: 14) 12 | fileprivate let stackViewOffset: CGFloat = 6 13 | 14 | class ActionCell: UICollectionViewCell { 15 | 16 | fileprivate let stackView: UIStackView = { 17 | let stackView = UIStackView() 18 | stackView.axis = .vertical 19 | stackView.distribution = .fillEqually 20 | stackView.spacing = spacing.x/2 21 | 22 | return stackView 23 | }() 24 | 25 | fileprivate let chevronImageView: UIImageView = { 26 | let bundle = Bundle(for: ImagePickerTrayController.self) 27 | let image = UIImage(named: "ActionCell-Chevron", in: bundle, compatibleWith: nil) 28 | let imageView = UIImageView(image: image) 29 | imageView.alpha = 0.5 30 | 31 | return imageView 32 | }() 33 | 34 | var actions = [ImagePickerAction]() { 35 | // It is sufficient to compare the length of the array 36 | // as actions can only be added but not removed 37 | 38 | willSet { 39 | if newValue.count != actions.count { 40 | stackView.arrangedSubviews.forEach { $0.removeFromSuperview() } 41 | } 42 | } 43 | didSet { 44 | if stackView.arrangedSubviews.count != actions.count { 45 | actions.map { ActionButton(action: $0, target: self, selector: #selector(callAction(sender:))) } 46 | .forEach { stackView.addArrangedSubview($0) } 47 | } 48 | } 49 | } 50 | 51 | var disclosureProcess: CGFloat = 0 { 52 | didSet { 53 | setNeedsLayout() 54 | } 55 | } 56 | 57 | // MARK: - Initialization 58 | 59 | override init(frame: CGRect) { 60 | super.init(frame: frame) 61 | 62 | initialize() 63 | } 64 | 65 | required init?(coder aDecoder: NSCoder) { 66 | super.init(coder: aDecoder) 67 | 68 | initialize() 69 | } 70 | 71 | fileprivate func initialize() { 72 | contentView.addSubview(stackView) 73 | contentView.addSubview(chevronImageView) 74 | } 75 | 76 | // MARK: - Layout 77 | 78 | override func layoutSubviews() { 79 | super.layoutSubviews() 80 | 81 | let progress = max(disclosureProcess, 0) 82 | stackView.frame = bounds.insetBy(dx: spacing.x, dy: spacing.y).offsetBy(dx: -progress * stackViewOffset, dy: 0) 83 | 84 | chevronImageView.alpha = progress/2 85 | 86 | let chevronOffset = (1-progress) * (spacing.x + stackViewOffset) 87 | let chevronCenterX = max(bounds.maxX - spacing.x/2 + chevronOffset, stackView.frame.maxX + spacing.x/2) 88 | chevronImageView.center = CGPoint(x: chevronCenterX, y: bounds.midY) 89 | } 90 | 91 | // MARK: - 92 | 93 | @objc fileprivate func callAction(sender: UIButton) { 94 | if let index = stackView.arrangedSubviews.index(of: sender) { 95 | actions[index].call() 96 | } 97 | } 98 | 99 | } 100 | 101 | fileprivate class ActionButton: UIButton { 102 | 103 | // MARK: - Initialization 104 | 105 | init(action: ImagePickerAction, target: Any, selector: Selector) { 106 | super.init(frame: .zero) 107 | 108 | setTitle(action.title, for: .normal) 109 | setTitleColor(.black, for: .normal) 110 | setImage(action.image.withRenderingMode(.alwaysTemplate), for: .normal) 111 | 112 | imageView?.tintColor = .black 113 | imageView?.contentMode = .bottom 114 | 115 | titleLabel?.textAlignment = .center 116 | titleLabel?.font = .systemFont(ofSize: 14) 117 | 118 | backgroundColor = .white 119 | layer.masksToBounds = true 120 | layer.cornerRadius = 11.0 121 | addTarget(target, action: selector, for: .touchUpInside) 122 | } 123 | 124 | required init?(coder aDecoder: NSCoder) { 125 | fatalError("init(coder:) has not been implemented") 126 | } 127 | 128 | // MARK: - Layout 129 | 130 | fileprivate override func imageRect(forContentRect contentRect: CGRect) -> CGRect { 131 | return contentRect.divided(atDistance: contentRect.midX, from: .minYEdge).slice 132 | } 133 | 134 | fileprivate override func titleRect(forContentRect contentRect: CGRect) -> CGRect { 135 | return contentRect.divided(atDistance: contentRect.midX, from: .minYEdge).remainder 136 | } 137 | 138 | } 139 | -------------------------------------------------------------------------------- /ImagePickerTrayController/Cells/CameraCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraCell.swift 3 | // ImagePickerTrayController 4 | // 5 | // Created by Laurin Brandner on 15.10.16. 6 | // Copyright © 2016 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CameraCell: UICollectionViewCell { 12 | 13 | var cameraView: UIView? { 14 | willSet { 15 | cameraView?.removeFromSuperview() 16 | } 17 | didSet { 18 | if let cameraView = cameraView { 19 | contentView.addSubview(cameraView) 20 | } 21 | } 22 | } 23 | 24 | var cameraOverlayView: UIView? { 25 | didSet { 26 | setNeedsLayout() 27 | } 28 | } 29 | 30 | // MARK: - Layout 31 | 32 | override func layoutSubviews() { 33 | super.layoutSubviews() 34 | 35 | cameraView?.frame = bounds 36 | cameraOverlayView?.frame = bounds 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /ImagePickerTrayController/Cells/CameraOverlayView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CameraOverlayView.swift 3 | // ImagePickerTrayController 4 | // 5 | // Created by Laurin Brandner on 26.11.16. 6 | // Copyright © 2016 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class CameraOverlayView: UIButton { 12 | 13 | let flipCameraButton: UIButton = { 14 | let button = FlipCameraButton() 15 | button.setImage(UIImage(bundledName: "CameraOverlayView-CameraFlip"), for: .normal) 16 | 17 | return button 18 | }() 19 | 20 | fileprivate let shutterButtonView = ShutterButtonView() 21 | 22 | override var isHighlighted: Bool { 23 | didSet { 24 | shutterButtonView.isHighlighted = isHighlighted 25 | } 26 | } 27 | 28 | // MARK: - Initialization 29 | 30 | override init(frame: CGRect) { 31 | super.init(frame: frame) 32 | 33 | initialize() 34 | } 35 | 36 | required init?(coder aDecoder: NSCoder) { 37 | super.init(coder: aDecoder) 38 | 39 | initialize() 40 | } 41 | 42 | fileprivate func initialize() { 43 | addSubview(flipCameraButton) 44 | addSubview(shutterButtonView) 45 | } 46 | 47 | // MARK: - Layout 48 | 49 | override func layoutSubviews() { 50 | super.layoutSubviews() 51 | 52 | let flipCameraButtonSize = CGSize(width: 44, height: 44) 53 | let flipCameraButtonOrigin = CGPoint(x: bounds.maxX - flipCameraButtonSize.width, y: bounds.minY) 54 | flipCameraButton.frame = CGRect(origin: flipCameraButtonOrigin, size: flipCameraButtonSize) 55 | 56 | let shutterButtonViewSize = CGSize(width: 29, height: 29) 57 | let shutterButtonViewOrigin = CGPoint(x: bounds.midX - shutterButtonViewSize.width/2, y: bounds.maxY - shutterButtonViewSize.height-4) 58 | shutterButtonView.frame = CGRect(origin: shutterButtonViewOrigin, size: shutterButtonViewSize) 59 | } 60 | 61 | } 62 | 63 | fileprivate class FlipCameraButton: UIButton { 64 | 65 | fileprivate override func imageRect(forContentRect contentRect: CGRect) -> CGRect { 66 | let imageSize = super.imageRect(forContentRect: contentRect).size 67 | let imageOrigin = CGPoint(x: contentRect.maxX-imageSize.width-10, y: contentRect.minY+10) 68 | 69 | return CGRect(origin: imageOrigin, size: imageSize) 70 | } 71 | 72 | } 73 | 74 | fileprivate class ShutterButtonView: UIView { 75 | 76 | let bezelLayer: CALayer = { 77 | let layer = CALayer() 78 | layer.masksToBounds = true 79 | layer.borderColor = UIColor.white.cgColor 80 | layer.borderWidth = 2 81 | 82 | return layer 83 | }() 84 | 85 | let knobLayer: CALayer = { 86 | let layer = CALayer() 87 | layer.masksToBounds = true 88 | layer.backgroundColor = UIColor.white.cgColor 89 | 90 | return layer 91 | }() 92 | 93 | var isHighlighted = false { 94 | didSet { 95 | knobLayer.backgroundColor = (isHighlighted) ? UIColor.lightGray.cgColor : UIColor.white.cgColor 96 | } 97 | } 98 | 99 | // MARK: - Initialization 100 | 101 | override init(frame: CGRect) { 102 | super.init(frame: frame) 103 | 104 | initialize() 105 | } 106 | 107 | required init?(coder aDecoder: NSCoder) { 108 | super.init(coder: aDecoder) 109 | 110 | initialize() 111 | } 112 | 113 | fileprivate func initialize() { 114 | layer.addSublayer(bezelLayer) 115 | layer.addSublayer(knobLayer) 116 | } 117 | 118 | // MARK: - Layout 119 | 120 | override func layoutSubviews() { 121 | super.layoutSubviews() 122 | 123 | bezelLayer.frame = bounds 124 | bezelLayer.cornerRadius = bounds.width/2 125 | 126 | let knobInset = bezelLayer.borderWidth + 1.5 127 | knobLayer.frame = bounds.insetBy(dx: knobInset, dy: knobInset) 128 | knobLayer.cornerRadius = knobLayer.bounds.height/2 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /ImagePickerTrayController/Cells/ImageCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageCell.swift 3 | // ImagePickerTrayController 4 | // 5 | // Created by Laurin Brandner on 15.10.16. 6 | // Copyright © 2016 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ImageCell: UICollectionViewCell { 12 | 13 | let imageView: UIImageView = { 14 | let imageView = UIImageView() 15 | imageView.contentMode = .scaleAspectFill 16 | imageView.clipsToBounds = true 17 | 18 | return imageView 19 | }() 20 | 21 | fileprivate let shadowView = UIImageView(image: UIImage(bundledName: "ImageCell-Shadow")) 22 | 23 | fileprivate let videoIndicatorView = UIImageView(image: UIImage(bundledName: "ImageCell-Video")) 24 | 25 | fileprivate let cloudIndicatorView = UIImageView(image: UIImage(bundledName: "ImageCell-Cloud")) 26 | 27 | fileprivate let checkmarkView = UIImageView(image: UIImage(bundledName: "ImageCell-Selected")) 28 | 29 | var isVideo = false { 30 | didSet { 31 | reloadAccessoryViews() 32 | } 33 | } 34 | 35 | var isRemote = false { 36 | didSet { 37 | reloadAccessoryViews() 38 | } 39 | } 40 | 41 | override var isSelected: Bool { 42 | didSet { 43 | reloadCheckmarkView() 44 | } 45 | } 46 | 47 | // MARK: - Initialization 48 | 49 | override init(frame: CGRect) { 50 | super.init(frame: frame) 51 | 52 | initialize() 53 | } 54 | 55 | required init?(coder aDecoder: NSCoder) { 56 | super.init(coder: aDecoder) 57 | 58 | initialize() 59 | } 60 | 61 | fileprivate func initialize() { 62 | contentView.addSubview(imageView) 63 | contentView.addSubview(shadowView) 64 | contentView.addSubview(videoIndicatorView) 65 | contentView.addSubview(cloudIndicatorView) 66 | contentView.addSubview(checkmarkView) 67 | 68 | reloadAccessoryViews() 69 | reloadCheckmarkView() 70 | } 71 | 72 | // MARK: - Other Methods 73 | 74 | fileprivate func reloadAccessoryViews() { 75 | videoIndicatorView.isHidden = !isVideo 76 | cloudIndicatorView.isHidden = !isRemote 77 | shadowView.isHidden = videoIndicatorView.isHidden && cloudIndicatorView.isHidden 78 | } 79 | 80 | fileprivate func reloadCheckmarkView() { 81 | checkmarkView.isHidden = !isSelected 82 | } 83 | 84 | override func prepareForReuse() { 85 | super.prepareForReuse() 86 | 87 | imageView.image = nil 88 | isVideo = false 89 | isRemote = false 90 | } 91 | 92 | // MARK: - Layout 93 | 94 | override func layoutSubviews() { 95 | super.layoutSubviews() 96 | 97 | imageView.frame = bounds 98 | let inset: CGFloat = 8 99 | 100 | let shadowHeight = shadowView.image?.size.height ?? 0 101 | shadowView.frame = CGRect(origin: CGPoint(x: bounds.minX, y: bounds.maxY-shadowHeight), size: CGSize(width: bounds.width, height: shadowHeight)) 102 | 103 | let videoIndicatorViewSize = videoIndicatorView.image?.size ?? .zero 104 | let videoIndicatorViewOrigin = CGPoint(x: bounds.minX + inset, y: bounds.maxY - inset - videoIndicatorViewSize.height) 105 | videoIndicatorView.frame = CGRect(origin: videoIndicatorViewOrigin, size: videoIndicatorViewSize) 106 | 107 | let cloudIndicatorViewSize = cloudIndicatorView.image?.size ?? .zero 108 | let cloudIndicatorViewOrigin = CGPoint(x: bounds.maxX - inset - cloudIndicatorViewSize.width, y: bounds.maxY - inset - cloudIndicatorViewSize.height) 109 | cloudIndicatorView.frame = CGRect(origin: cloudIndicatorViewOrigin, size: cloudIndicatorViewSize) 110 | 111 | let checkmarkSize = checkmarkView.frame.size 112 | checkmarkView.center = CGPoint(x: bounds.maxX-checkmarkSize.width/2-4, y: bounds.maxY-checkmarkSize.height/2-4) 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /ImagePickerTrayController/Helpers/UIEdgeInsetsExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIEdgeInsetsExtensions.swift 3 | // ImagePickerTrayController 4 | // 5 | // Created by Laurin Brandner on 27.11.16. 6 | // Copyright © 2016 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIEdgeInsets { 12 | 13 | var vertical: CGFloat { 14 | return top + bottom 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /ImagePickerTrayController/Helpers/UIImageExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImageExtensions.swift 3 | // ImagePickerTrayController 4 | // 5 | // Created by Laurin Brandner on 24.11.16. 6 | // Copyright © 2016 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | extension UIImage { 12 | 13 | convenience init?(bundledName name: String) { 14 | let bundle = Bundle(for: ImagePickerTrayController.self) 15 | self.init(named: name, in: bundle, compatibleWith:nil) 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /ImagePickerTrayController/ImagePickerAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerAction.swift 3 | // ImagePickerTrayController 4 | // 5 | // Created by Laurin Brandner on 22.11.16. 6 | // Copyright © 2016 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct ImagePickerAction { 12 | 13 | public typealias Callback = (ImagePickerAction) -> () 14 | 15 | public var title: String 16 | public var image: UIImage 17 | public var callback: Callback 18 | 19 | public static func cameraAction(with callback: @escaping Callback) -> ImagePickerAction { 20 | let image = UIImage(bundledName: "ImagePickerAction-Camera")! 21 | 22 | return ImagePickerAction(title: NSLocalizedString("Camera", comment: "Image Picker Camera Action"), image: image, callback: callback) 23 | } 24 | 25 | public static func libraryAction(with callback: @escaping Callback) -> ImagePickerAction { 26 | let image = UIImage(bundledName: "ImagePickerAction-Library")! 27 | 28 | return ImagePickerAction(title: NSLocalizedString("Photo Library", comment: "Image Picker Photo Library Action"), image: image, callback: callback) 29 | } 30 | 31 | func call() { 32 | callback(self) 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /ImagePickerTrayController/ImagePickerTrayController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerTrayController.h 3 | // ImagePickerTrayController 4 | // 5 | // Created by Laurin Brandner on 14.10.16. 6 | // Copyright © 2016 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for ImagePickerTrayController. 12 | FOUNDATION_EXPORT double ImagePickerTrayControllerVersionNumber; 13 | 14 | //! Project version string for ImagePickerTrayController. 15 | FOUNDATION_EXPORT const unsigned char ImagePickerTrayControllerVersionString[]; 16 | -------------------------------------------------------------------------------- /ImagePickerTrayController/ImagePickerTrayController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerTrayController.swift 3 | // ImagePickerTrayController 4 | // 5 | // Created by Laurin Brandner on 14.10.16. 6 | // Copyright © 2016 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | 12 | fileprivate let itemSpacing: CGFloat = 1 13 | 14 | /// The media type an instance of ImagePickerSheetController can display 15 | public enum ImagePickerMediaType { 16 | case image 17 | case video 18 | case imageAndVideo 19 | } 20 | 21 | @objc public protocol ImagePickerTrayControllerDelegate { 22 | 23 | @objc optional func controller(_ controller: ImagePickerTrayController, willSelectAsset asset: PHAsset) 24 | @objc optional func controller(_ controller: ImagePickerTrayController, didSelectAsset asset: PHAsset) 25 | 26 | @objc optional func controller(_ controller: ImagePickerTrayController, willDeselectAsset asset: PHAsset) 27 | @objc optional func controller(_ controller: ImagePickerTrayController, didDeselectAsset asset: PHAsset) 28 | 29 | @objc optional func controller(_ controller: ImagePickerTrayController, didTakeImage image:UIImage) 30 | 31 | } 32 | 33 | public let ImagePickerTrayWillShow: Notification.Name = Notification.Name(rawValue: "ch.laurinbrandner.ImagePickerTrayWillShow") 34 | public let ImagePickerTrayDidShow: Notification.Name = Notification.Name(rawValue: "ch.laurinbrandner.ImagePickerTrayDidShow") 35 | 36 | public let ImagePickerTrayWillHide: Notification.Name = Notification.Name(rawValue: "ch.laurinbrandner.ImagePickerTrayWillHide") 37 | public let ImagePickerTrayDidHide: Notification.Name = Notification.Name(rawValue: "ch.laurinbrandner.ImagePickerTrayDidHide") 38 | 39 | public let ImagePickerTrayFrameUserInfoKey = "ImagePickerTrayFrame" 40 | public let ImagePickerTrayAnimationDurationUserInfoKey = "ImagePickerTrayAnimationDuration" 41 | 42 | fileprivate let animationDuration: TimeInterval = 0.2 43 | 44 | public class ImagePickerTrayController: UIViewController { 45 | 46 | fileprivate(set) lazy var collectionView: UICollectionView = { 47 | let layout = UICollectionViewFlowLayout() 48 | layout.scrollDirection = .horizontal 49 | layout.minimumInteritemSpacing = itemSpacing 50 | layout.minimumLineSpacing = itemSpacing 51 | 52 | let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout) 53 | collectionView.contentInset = UIEdgeInsets(top: 1, left: 0, bottom: 2, right: 1) 54 | collectionView.translatesAutoresizingMaskIntoConstraints = false 55 | collectionView.backgroundColor = UIColor(red: 209.0/255.0, green: 213.0/255.0, blue: 218.0/255.0, alpha: 1.0) 56 | collectionView.dataSource = self 57 | collectionView.delegate = self 58 | collectionView.showsHorizontalScrollIndicator = false 59 | collectionView.alwaysBounceHorizontal = true 60 | 61 | collectionView.register(ActionCell.self, forCellWithReuseIdentifier: NSStringFromClass(ActionCell.self)) 62 | collectionView.register(CameraCell.self, forCellWithReuseIdentifier: NSStringFromClass(CameraCell.self)) 63 | collectionView.register(ImageCell.self, forCellWithReuseIdentifier: NSStringFromClass(ImageCell.self)) 64 | 65 | return collectionView 66 | }() 67 | 68 | fileprivate lazy var cameraController: UIImagePickerController = { 69 | let controller = UIImagePickerController() 70 | controller.delegate = self 71 | controller.sourceType = .camera 72 | controller.showsCameraControls = false 73 | controller.allowsEditing = false 74 | controller.cameraFlashMode = .off 75 | 76 | let view = CameraOverlayView() 77 | view.addTarget(self, action: #selector(takePicture), for: .touchUpInside) 78 | view.flipCameraButton.addTarget(self, action: #selector(flipCamera), for: .touchUpInside) 79 | controller.cameraOverlayView = view 80 | 81 | return controller 82 | }() 83 | 84 | fileprivate let imageManager = PHCachingImageManager() 85 | fileprivate var assets = [PHAsset]() 86 | fileprivate lazy var requestOptions: PHImageRequestOptions = { 87 | let options = PHImageRequestOptions() 88 | options.deliveryMode = .highQualityFormat 89 | options.resizeMode = .fast 90 | 91 | return options 92 | }() 93 | 94 | 95 | public var allowsMultipleSelection = true { 96 | didSet { 97 | if isViewLoaded { 98 | collectionView.allowsMultipleSelection = allowsMultipleSelection 99 | } 100 | } 101 | } 102 | 103 | fileprivate var imageSize: CGSize = .zero 104 | let trayHeight: CGFloat 105 | 106 | fileprivate let actionCellWidth: CGFloat = 162 107 | fileprivate weak var actionCell: ActionCell? 108 | 109 | public fileprivate(set) var actions = [ImagePickerAction]() 110 | 111 | fileprivate var sections: [Int] { 112 | let actionSection = (actions.count > 0) ? 1 : 0 113 | let cameraSection = UIImagePickerController.isSourceTypeAvailable(.camera) ? 1 : 0 114 | let assetSection = assets.count 115 | 116 | return [actionSection, cameraSection, assetSection] 117 | } 118 | 119 | public var delegate: ImagePickerTrayControllerDelegate? 120 | 121 | /// If set to `true` the tray can be dragged down in order to dismiss it 122 | /// Defaults to `true` 123 | public var allowsInteractivePresentation: Bool { 124 | get { 125 | return transitionController?.allowsInteractiveTransition ?? false 126 | } 127 | set { 128 | transitionController?.allowsInteractiveTransition = newValue 129 | } 130 | } 131 | private var transitionController: TransitionController? 132 | 133 | // MARK: - Initialization 134 | 135 | public init() { 136 | self.trayHeight = 216 137 | 138 | super.init(nibName: nil, bundle: nil) 139 | 140 | transitionController = TransitionController(trayController: self) 141 | modalPresentationStyle = .custom 142 | transitioningDelegate = transitionController 143 | } 144 | 145 | public required init?(coder aDecoder: NSCoder) { 146 | fatalError("init(coder:) has not been implemented") 147 | } 148 | 149 | // MARK: - View Lifecycle 150 | 151 | public override func loadView() { 152 | super.loadView() 153 | 154 | view.addSubview(collectionView) 155 | collectionView.heightAnchor.constraint(equalToConstant: trayHeight).isActive = true 156 | collectionView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true 157 | collectionView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true 158 | collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true 159 | collectionView.allowsMultipleSelection = allowsMultipleSelection 160 | 161 | let numberOfRows = (UIDevice.current.userInterfaceIdiom == .pad) ? 3 : 2 162 | let totalItemSpacing = CGFloat(numberOfRows-1)*itemSpacing + collectionView.contentInset.vertical 163 | let side = round((self.trayHeight-totalItemSpacing)/CGFloat(numberOfRows)) 164 | self.imageSize = CGSize(width: side, height: side) 165 | } 166 | 167 | public override func viewWillAppear(_ animated: Bool) { 168 | super.viewWillAppear(animated) 169 | 170 | fetchAssets() 171 | } 172 | 173 | public override func viewDidLayoutSubviews() { 174 | super.viewDidLayoutSubviews() 175 | 176 | collectionView.setContentOffset(CGPoint(x: actionCellWidth - spacing.x, y: 0), animated: false) 177 | reloadActionCellDisclosureProgress() 178 | } 179 | 180 | // MARK: - Action 181 | 182 | public func add(action: ImagePickerAction) { 183 | actions.append(action) 184 | } 185 | 186 | // MARK: - Images 187 | 188 | fileprivate func prepareAssets() { 189 | fetchAssets() 190 | } 191 | 192 | fileprivate func fetchAssets() { 193 | let options = PHFetchOptions() 194 | options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] 195 | options.fetchLimit = 100 196 | 197 | let result = PHAsset.fetchAssets(with: options) 198 | result.enumerateObjects({ asset, index, stop in 199 | self.assets.append(asset) 200 | }) 201 | } 202 | 203 | fileprivate func requestImage(for asset: PHAsset, completion: @escaping (_ image: UIImage?) -> ()) { 204 | requestOptions.isSynchronous = true 205 | let size = scale(imageSize: imageSize) 206 | 207 | // Workaround because PHImageManager.requestImageForAsset doesn't work for burst images 208 | if asset.representsBurst { 209 | imageManager.requestImageData(for: asset, options: requestOptions) { data, _, _, _ in 210 | let image = data.flatMap { UIImage(data: $0) } 211 | completion(image) 212 | } 213 | } 214 | else { 215 | imageManager.requestImage(for: asset, targetSize: size, contentMode: .aspectFill, options: requestOptions) { image, _ in 216 | completion(image) 217 | } 218 | } 219 | } 220 | 221 | fileprivate func prefetchImages(for asset: PHAsset) { 222 | let size = scale(imageSize: imageSize) 223 | imageManager.startCachingImages(for: [asset], targetSize: size, contentMode: .aspectFill, options: requestOptions) 224 | } 225 | 226 | fileprivate func scale(imageSize size: CGSize) -> CGSize { 227 | let scale = UIScreen.main.scale 228 | return CGSize(width: size.width * scale, height: size.height * scale) 229 | } 230 | 231 | // MARK: - Camera 232 | 233 | @objc fileprivate func flipCamera() { 234 | cameraController.cameraDevice = (cameraController.cameraDevice == .rear) ? .front : .rear 235 | } 236 | 237 | @objc fileprivate func takePicture() { 238 | cameraController.takePicture() 239 | } 240 | 241 | // MARK: - 242 | 243 | fileprivate func reloadActionCellDisclosureProgress() { 244 | if sections[0] > 0 { 245 | actionCell?.disclosureProcess = (collectionView.contentOffset.x / (actionCellWidth/2)) 246 | } 247 | } 248 | 249 | fileprivate func post(name: Notification.Name, frame: CGRect, duration: TimeInterval?) { 250 | var userInfo: [AnyHashable: Any] = [ImagePickerTrayFrameUserInfoKey: frame] 251 | if let duration = duration { 252 | userInfo[ImagePickerTrayAnimationDurationUserInfoKey] = duration 253 | } 254 | 255 | NotificationCenter.default.post(name: name, object: self, userInfo: userInfo) 256 | } 257 | 258 | } 259 | 260 | // MARK: - UICollectionViewDataSource 261 | 262 | extension ImagePickerTrayController: UICollectionViewDataSource { 263 | 264 | public func numberOfSections(in collectionView: UICollectionView) -> Int { 265 | return sections.count 266 | } 267 | 268 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 269 | return sections[section] 270 | } 271 | 272 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 273 | switch indexPath.section { 274 | case 0: 275 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(ActionCell.self), for: indexPath) as! ActionCell 276 | cell.actions = actions 277 | actionCell = cell 278 | reloadActionCellDisclosureProgress() 279 | 280 | return cell 281 | case 1: 282 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(CameraCell.self), for: indexPath) as! CameraCell 283 | cell.cameraView = cameraController.view 284 | cell.cameraOverlayView = cameraController.cameraOverlayView 285 | 286 | return cell 287 | case 2: 288 | let asset = assets[indexPath.item] 289 | 290 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(ImageCell.self), for: indexPath) as! ImageCell 291 | cell.isVideo = (asset.mediaType == .video) 292 | cell.isRemote = (asset.sourceType != .typeUserLibrary) 293 | requestImage(for: asset) { cell.imageView.image = $0 } 294 | 295 | return cell 296 | default: 297 | fatalError("More than 3 sections is invalid.") 298 | } 299 | } 300 | 301 | } 302 | 303 | // MARK: - UICollectionViewDelegate 304 | 305 | extension ImagePickerTrayController: UICollectionViewDelegate { 306 | 307 | public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool { 308 | guard indexPath.section == sections.count - 1 else { 309 | return false 310 | } 311 | 312 | delegate?.controller?(self, willSelectAsset: assets[indexPath.item]) 313 | 314 | return true 315 | } 316 | 317 | public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 318 | delegate?.controller?(self, didSelectAsset: assets[indexPath.item]) 319 | } 320 | 321 | public func collectionView(_ collectionView: UICollectionView, shouldDeselectItemAt indexPath: IndexPath) -> Bool { 322 | delegate?.controller?(self, willDeselectAsset: assets[indexPath.item]) 323 | 324 | return true 325 | } 326 | 327 | public func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { 328 | delegate?.controller?(self, didDeselectAsset: assets[indexPath.item]) 329 | } 330 | 331 | } 332 | 333 | // MARK: - UICollectionViewDelegateFlowLayout 334 | 335 | extension ImagePickerTrayController: UICollectionViewDelegateFlowLayout { 336 | 337 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 338 | let maxItemHeight = collectionView.frame.height-collectionView.contentInset.vertical 339 | 340 | switch indexPath.section { 341 | case 0: 342 | return CGSize(width: actionCellWidth, height: maxItemHeight) 343 | case 1: 344 | return CGSize(width: 150, height: maxItemHeight) 345 | case 2: 346 | return imageSize 347 | default: 348 | return .zero 349 | } 350 | } 351 | 352 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { 353 | guard section == 1 else { 354 | return UIEdgeInsets() 355 | } 356 | 357 | return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 6) 358 | } 359 | 360 | } 361 | 362 | // MARK: - UIScrollViewDelegate 363 | 364 | extension ImagePickerTrayController: UIScrollViewDelegate { 365 | 366 | public func scrollViewDidScroll(_ scrollView: UIScrollView) { 367 | reloadActionCellDisclosureProgress() 368 | } 369 | } 370 | 371 | // MARK: - UIImagePickerControllerDelegate 372 | 373 | extension ImagePickerTrayController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { 374 | 375 | public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { 376 | if let image = info[UIImagePickerControllerOriginalImage] as? UIImage { 377 | delegate?.controller?(self, didTakeImage: image) 378 | } 379 | } 380 | 381 | } 382 | -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ActionCell-Chevron.imageset/ActionCell-Chevron@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ActionCell-Chevron.imageset/ActionCell-Chevron@2x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ActionCell-Chevron.imageset/ActionCell-Chevron@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ActionCell-Chevron.imageset/ActionCell-Chevron@3x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ActionCell-Chevron.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ActionCell-Chevron@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ActionCell-Chevron@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/CameraOverlayView-CameraFlip.imageset/CameraOverlayView-CameraFlip@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/CameraOverlayView-CameraFlip.imageset/CameraOverlayView-CameraFlip@2x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/CameraOverlayView-CameraFlip.imageset/CameraOverlayView-CameraFlip@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/CameraOverlayView-CameraFlip.imageset/CameraOverlayView-CameraFlip@3x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/CameraOverlayView-CameraFlip.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "CameraOverlayView-CameraFlip@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "CameraOverlayView-CameraFlip@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImageCell-Cloud.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ImageCell-Cloud@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ImageCell-Cloud@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImageCell-Cloud.imageset/ImageCell-Cloud@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ImageCell-Cloud.imageset/ImageCell-Cloud@2x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImageCell-Cloud.imageset/ImageCell-Cloud@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ImageCell-Cloud.imageset/ImageCell-Cloud@3x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImageCell-Selected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ImageCell-Selected@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ImageCell-Selected@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImageCell-Selected.imageset/ImageCell-Selected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ImageCell-Selected.imageset/ImageCell-Selected@2x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImageCell-Selected.imageset/ImageCell-Selected@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ImageCell-Selected.imageset/ImageCell-Selected@3x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImageCell-Shadow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ImageCell-Shadow@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ImageCell-Shadow@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImageCell-Shadow.imageset/ImageCell-Shadow@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ImageCell-Shadow.imageset/ImageCell-Shadow@2x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImageCell-Shadow.imageset/ImageCell-Shadow@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ImageCell-Shadow.imageset/ImageCell-Shadow@3x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImageCell-Unselected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ImageCell-Unselected@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ImageCell-Unselected@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImageCell-Unselected.imageset/ImageCell-Unselected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ImageCell-Unselected.imageset/ImageCell-Unselected@2x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImageCell-Unselected.imageset/ImageCell-Unselected@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ImageCell-Unselected.imageset/ImageCell-Unselected@3x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImageCell-Video.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ImageCell-Video@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ImageCell-Video@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImageCell-Video.imageset/ImageCell-Video@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ImageCell-Video.imageset/ImageCell-Video@2x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImageCell-Video.imageset/ImageCell-Video@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ImageCell-Video.imageset/ImageCell-Video@3x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImagePickerAction-Camera.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ImagePickerAction-Camera@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ImagePickerAction-Camera@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImagePickerAction-Camera.imageset/ImagePickerAction-Camera@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ImagePickerAction-Camera.imageset/ImagePickerAction-Camera@2x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImagePickerAction-Camera.imageset/ImagePickerAction-Camera@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ImagePickerAction-Camera.imageset/ImagePickerAction-Camera@3x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImagePickerAction-Library.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "ImagePickerAction-Library@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "ImagePickerAction-Library@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImagePickerAction-Library.imageset/ImagePickerAction-Library@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ImagePickerAction-Library.imageset/ImagePickerAction-Library@2x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/Images.xcassets/ImagePickerAction-Library.imageset/ImagePickerAction-Library@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/ImagePickerTrayController/Images.xcassets/ImagePickerAction-Library.imageset/ImagePickerAction-Library@3x.png -------------------------------------------------------------------------------- /ImagePickerTrayController/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 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSPrincipalClass 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /ImagePickerTrayController/TransitionController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TransitionController.swift 3 | // ImagePickerTrayController 4 | // 5 | // Created by Laurin Brandner on 17.04.17. 6 | // Copyright © 2017 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | class TransitionController: NSObject { 12 | 13 | fileprivate weak var trayController: ImagePickerTrayController? 14 | 15 | var allowsInteractiveTransition = true 16 | 17 | fileprivate let gestureRecognizer = UIPanGestureRecognizer() 18 | fileprivate var interactiveTransition: UIPercentDrivenInteractiveTransition? 19 | fileprivate var panDirection: CGFloat = 0 20 | 21 | init(trayController: ImagePickerTrayController) { 22 | self.trayController = trayController 23 | super.init() 24 | 25 | gestureRecognizer.addTarget(self, action: #selector(didRecognizePan(gestureRecognizer:))) 26 | gestureRecognizer.delegate = self 27 | } 28 | 29 | fileprivate func cancel() { 30 | interactiveTransition?.cancel() 31 | interactiveTransition = nil 32 | gestureRecognizer.cancel() 33 | } 34 | 35 | fileprivate func finish() { 36 | interactiveTransition?.finish() 37 | interactiveTransition = nil 38 | gestureRecognizer.cancel() 39 | } 40 | 41 | } 42 | 43 | // MARK: - UIViewControllerTransitioningDelegate 44 | 45 | extension TransitionController: UIViewControllerTransitioningDelegate { 46 | 47 | func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 48 | return AnimationController(transition: .presentation(gestureRecognizer)) 49 | } 50 | 51 | func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 52 | return AnimationController(transition: .dismissal) 53 | } 54 | 55 | func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? { 56 | return interactiveTransition 57 | } 58 | 59 | } 60 | 61 | // MARK: - UIGestureRecognizerDelegate 62 | 63 | extension TransitionController: UIGestureRecognizerDelegate { 64 | 65 | func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { 66 | return allowsInteractiveTransition 67 | } 68 | 69 | func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 70 | return true 71 | } 72 | 73 | @objc fileprivate func didRecognizePan(gestureRecognizer: UIPanGestureRecognizer) { 74 | guard let trayController = trayController, 75 | let view = gestureRecognizer.view else { 76 | cancel() 77 | return 78 | } 79 | 80 | if gestureRecognizer.state == .began { 81 | gestureRecognizer.setTranslation(.zero, in: gestureRecognizer.view) 82 | } 83 | else if gestureRecognizer.state == .changed { 84 | let end = gestureRecognizer.location(in: gestureRecognizer.view).y 85 | let translation = gestureRecognizer.translation(in: gestureRecognizer.view) 86 | let start = end - translation.y 87 | let threshold = view.frame.maxY - trayController.trayHeight 88 | if let transition = interactiveTransition { 89 | let progress = end-threshold 90 | transition.update(progress/trayController.trayHeight) 91 | } 92 | if start < threshold && end >= threshold && interactiveTransition == nil { 93 | interactiveTransition = UIPercentDrivenInteractiveTransition() 94 | trayController.dismiss(animated: true, completion: nil) 95 | } 96 | } 97 | else if gestureRecognizer.state == .cancelled { 98 | interactiveTransition?.completionSpeed = 0.95 99 | cancel() 100 | } 101 | else if gestureRecognizer.state == .ended { 102 | let velocity = gestureRecognizer.velocity(in: gestureRecognizer.view).y 103 | 104 | // This needs to be set, otherwise the cancel animation glitches from time to time 105 | // If you figure out how to fix this, let me know 106 | interactiveTransition?.completionSpeed = 0.95 107 | 108 | if velocity <= 0 { 109 | cancel() 110 | } 111 | else { 112 | finish() 113 | } 114 | } 115 | } 116 | 117 | } 118 | 119 | fileprivate extension UIPanGestureRecognizer { 120 | 121 | fileprivate func cancel() { 122 | isEnabled = false 123 | isEnabled = true 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /ImagePickerTrayControllerTests/ImagePickerTrayControllerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerTrayControllerTests.swift 3 | // ImagePickerTrayControllerTests 4 | // 5 | // Created by Laurin Brandner on 14.10.16. 6 | // Copyright © 2016 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import ImagePickerTrayController 11 | 12 | class ImagePickerTrayControllerTests: 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 | -------------------------------------------------------------------------------- /ImagePickerTrayControllerTests/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 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Laurin Brandner 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ImagePickerTrayController 2 | 3 | [![Twitter: @lbrndnr](https://img.shields.io/badge/contact-@lbrndnr-blue.svg?style=flat)](https://twitter.com/lbrndnr) 4 | [![License](http://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://github.com/lbrndnr/ImagePickerTrayController/blob/master/LICENSE) 5 | 6 | ## About 7 | ImagePickerTrayController is a component that replicates the custom photo action sheet in iMessage. It's the iOS 10 version of [ImagePickerSheetController](https://github.com/lbrndnr/ImagePickerSheetController). 8 | ⚠️Note that this library is still WIP⚠️ 9 | 10 | 11 | 12 | ### Example 13 | 14 | ```swift 15 | let controller = ImagePickerTrayController() 16 | controller.add(action: .cameraAction { _ in 17 | print("Show Camera") 18 | }) 19 | controller.add(action: .libraryAction { _ in 20 | print("Show Library") 21 | }) 22 | controller.show(in: view) 23 | imagePickerTrayController = controller 24 | ``` 25 | 26 | ## Installation 27 | 28 | I wouldn't recommend using this library just yet. It's still WIP⚠️. 29 | 30 | ## Requirements 31 | `ImagePickerTrayController` is written in Swift and links against `Photos.framework`. It therefore requires iOS 8 or later. 32 | 33 | ## Author 34 | I'm Laurin Brandner, I'm on [Twitter](https://twitter.com/lbrndnr). 35 | 36 | ## License 37 | `ImagePickerTrayController` is licensed under the [MIT License](http://opensource.org/licenses/mit-license.php). 38 | -------------------------------------------------------------------------------- /Screenshots/Example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerTrayController/09a6e26d16c2633ff068cc0539b3fe388077f6e5/Screenshots/Example.png --------------------------------------------------------------------------------