├── .gitignore ├── Cartfile.private ├── Cartfile.resolved ├── ImagePickerSheetController.podspec ├── ImagePickerSheetController ├── Example │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── LaunchScreen.xib │ │ ├── Localizable.strings │ │ └── Localizable.stringsdict │ ├── Info.plist │ └── ViewController.swift ├── ImagePickerSheetController.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ ├── ImagePickerSheetController.xccheckout │ │ │ └── WorkspaceSettings.xcsettings │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── ImagePickerSheetController.xcscheme │ └── xcuserdata │ │ ├── Laurin.xcuserdatad │ │ └── xcschemes │ │ │ ├── Example.xcscheme │ │ │ └── xcschememanagement.plist │ │ └── patrickbalestra.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── ImagePickerSheetController │ ├── AnimationController.swift │ ├── ImagePickerAction.swift │ ├── ImagePickerSheetController.h │ ├── ImagePickerSheetController.swift │ ├── Images.xcassets │ │ ├── Contents.json │ │ ├── PreviewCollectionViewCell-video.imageset │ │ │ ├── Contents.json │ │ │ ├── PreviewCollectionViewCell-video.png │ │ │ ├── PreviewCollectionViewCell-video@2x.png │ │ │ └── PreviewCollectionViewCell-video@3x.png │ │ ├── PreviewSupplementaryView-Checkmark-Selected.imageset │ │ │ ├── Contents.json │ │ │ ├── PreviewSupplementaryView-Checkmark-Selected.png │ │ │ ├── PreviewSupplementaryView-Checkmark-Selected@2x.png │ │ │ └── PreviewSupplementaryView-Checkmark-Selected@3x.png │ │ └── PreviewSupplementaryView-Checkmark.imageset │ │ │ ├── Contents.json │ │ │ ├── PreviewSupplementaryView-Checkmark.png │ │ │ ├── PreviewSupplementaryView-Checkmark@2x.png │ │ │ └── PreviewSupplementaryView-Checkmark@3x.png │ ├── Info.plist │ └── Sheet │ │ ├── Preview │ │ ├── PreviewCollectionView.swift │ │ ├── PreviewCollectionViewCell.swift │ │ ├── PreviewCollectionViewLayout.swift │ │ └── PreviewSupplementaryView.swift │ │ ├── SheetActionCollectionViewCell.swift │ │ ├── SheetCollectionViewCell.swift │ │ ├── SheetCollectionViewLayout.swift │ │ ├── SheetController.swift │ │ └── SheetPreviewCollectionViewCell.swift └── ImagePickerSheetControllerTests │ ├── ActionHandlingTests.swift │ ├── AddingActionTests.swift │ ├── DismissalTests.swift │ ├── ImagePickerSheetControllerTests.swift │ ├── ImageSelectionTests.swift │ ├── Info.plist │ ├── KIFExtensions.swift │ └── PresentationTests.swift ├── LICENSE ├── README.md └── Screenshots ├── GoT.gif └── Nature.png /.gitignore: -------------------------------------------------------------------------------- 1 | ImagePickerSheetController/ImagePickerSheetController.xcodeproj/project.xcworkspace/xcuserdata 2 | ImagePickerSheetController/ImagePickerSheetController.xcodeproj/xcuserdata/ 3 | -------------------------------------------------------------------------------- /Cartfile.private: -------------------------------------------------------------------------------- 1 | github "Quick/Nimble" "v2.0.0-rc.3" 2 | github "lbrndnr/KIF" "c6a3ce99baab225848e6ab975956454e1d49ee6a" -------------------------------------------------------------------------------- /Cartfile.resolved: -------------------------------------------------------------------------------- 1 | github "larcus94/KIF" "c6a3ce99baab225848e6ab975956454e1d49ee6a" 2 | github "Quick/Nimble" "v2.0.0-rc.3" 3 | -------------------------------------------------------------------------------- /ImagePickerSheetController.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # Be sure to run `pod spec lint ImagePickerSheetController.podspec' to ensure this is a 3 | # valid spec and to remove all comments including this before submitting the spec. 4 | # 5 | # To learn more about Podspec attributes see http://docs.cocoapods.org/specification.html 6 | # To see working Podspecs in the CocoaPods repo see https://github.com/CocoaPods/Specs/ 7 | # 8 | 9 | Pod::Spec.new do |s| 10 | 11 | # ――― Spec Metadata ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 12 | # 13 | # These will help people to find your library, and whilst it 14 | # can feel like a chore to fill in it's definitely to your advantage. The 15 | # summary should be tweet-length, and the description more in depth. 16 | # 17 | 18 | s.name = "ImagePickerSheetController" 19 | s.version = "0.9.3" 20 | s.summary = "ImagePickerSheetController is like the custom photo action sheet found in the iOS 8 and 9 version of iMessage" 21 | 22 | s.description = <<-DESC 23 | ImagePickerSheetController is a component that replicates the custom photo action sheet in iMessage. It's very similar to UIAlertController which makes its usage simple and concise. 24 | DESC 25 | 26 | s.homepage = "https://github.com/lbrndnr/ImagePickerSheetController" 27 | s.screenshot = "https://raw.githubusercontent.com/lbrndnr/ImagePickerSheetController/master/Screenshots/Nature.png" 28 | 29 | 30 | # ――― Spec License ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 31 | # 32 | # Licensing your code is important. See http://choosealicense.com for more info. 33 | # CocoaPods will detect a license file if there is a named LICENSE* 34 | # Popular ones are 'MIT', 'BSD' and 'Apache License, Version 2.0'. 35 | # 36 | 37 | s.license = { :type => "MIT", :file => "LICENSE" } 38 | 39 | 40 | # ――― Author Metadata ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 41 | # 42 | # Specify the authors of the library, with email addresses. Email addresses 43 | # of the authors are extracted from the SCM log. E.g. $ git log. CocoaPods also 44 | # accepts just a name if you'd rather not provide an email address. 45 | # 46 | # Specify a social_media_url where others can refer to, for example a twitter 47 | # profile URL. 48 | # 49 | 50 | s.author = { "Laurin Brandner" => "hello@laurinbrandner.ch" } 51 | s.social_media_url = "http://twitter.com/lbrndnr" 52 | 53 | # ――― Platform Specifics ――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 54 | # 55 | # If this Pod runs only on iOS or OS X, then specify the platform and 56 | # the deployment target. You can optionally include the target after the platform. 57 | # 58 | 59 | s.platform = :ios, "9.0" 60 | 61 | # When using multiple platforms 62 | s.ios.deployment_target = "9.0" 63 | # s.osx.deployment_target = "10.7" 64 | 65 | 66 | # ――― Source Location ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 67 | # 68 | # Specify the location from where the source should be retrieved. 69 | # Supports git, hg, bzr, svn and HTTP. 70 | # 71 | 72 | s.source = { :git => "https://github.com/lbrndnr/ImagePickerSheetController.git", :tag => s.version } 73 | 74 | 75 | # ――― Source Code ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 76 | # 77 | # CocoaPods is smart about how it includes source code. For source files 78 | # giving a folder will include any swift, h, m, mm, c & cpp files. 79 | # For header files it will include any header in the folder. 80 | # Not including the public_header_files will make all headers public. 81 | # 82 | 83 | s.source_files = "ImagePickerSheetController/ImagePickerSheetController/**/*.swift" 84 | 85 | 86 | # ――― Resources ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 87 | # 88 | # A list of resources included with the Pod. These are copied into the 89 | # target bundle with a build phase script. Anything else will be cleaned. 90 | # You can preserve files from being cleaned, please don't preserve 91 | # non-essential files like tests, examples and documentation. 92 | # 93 | 94 | s.resource = "ImagePickerSheetController/ImagePickerSheetController/Images.xcassets" 95 | # s.resources = "Resources/*.png" 96 | # s.resource_bundle = {"Images" => ["ImagePickerSheetController/ImagePickerSheetController/Images.xcassets"]} 97 | 98 | # s.preserve_paths = "FilesToSave", "MoreFilesToSave" 99 | 100 | 101 | # ――― Project Linking ―――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 102 | # 103 | # Link your library with frameworks, or libraries. Libraries do not include 104 | # the lib prefix of their name. 105 | # 106 | 107 | s.framework = "Photos" 108 | 109 | 110 | # ――― Project Settings ――――――――――――――――――――――――――――――――――――――――――――――――――――――――― # 111 | # 112 | # If your library depends on compiler flags you can set them in the xcconfig hash 113 | # where they will only apply to your library. If you depend on other Podspecs 114 | # you can include multiple dependencies to ensure it works. 115 | 116 | s.requires_arc = true 117 | 118 | 119 | end 120 | -------------------------------------------------------------------------------- /ImagePickerSheetController/Example/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Example 4 | // 5 | // Created by Laurin Brandner on 26/05/15. 6 | // Copyright (c) 2015 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: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { 22 | window?.rootViewController = ViewController() 23 | window?.makeKeyAndVisible() 24 | 25 | return true 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /ImagePickerSheetController/Example/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 20 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /ImagePickerSheetController/Example/Base.lproj/Localizable.strings: -------------------------------------------------------------------------------- 1 | /* 2 | Localizable.strings 3 | BRNImagePickerSheet 4 | 5 | Created by Laurin Brandner on 11/11/14. 6 | Copyright (c) 2014 Laurin Brandner. All rights reserved. 7 | */ 8 | -------------------------------------------------------------------------------- /ImagePickerSheetController/Example/Base.lproj/Localizable.stringsdict: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ImagePickerSheet.button1.Send %lu Photo 6 | 7 | NSStringLocalizedFormatKey 8 | Send %#@photos@ 9 | photos 10 | 11 | NSStringFormatSpecTypeKey 12 | NSStringPluralRuleType 13 | NSStringFormatValueTypeKey 14 | lu 15 | one 16 | %lu Photo 17 | other 18 | %lu Photos 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /ImagePickerSheetController/Example/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 | IPSC Example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | NSPhotoLibraryUsageDescription 38 | Please allow access to browse pictures. 39 | 40 | 41 | -------------------------------------------------------------------------------- /ImagePickerSheetController/Example/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Example 4 | // 5 | // Created by Laurin Brandner on 26/05/15. 6 | // Copyright (c) 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Photos 11 | import ImagePickerSheetController 12 | 13 | class ViewController: UIViewController { 14 | 15 | // MARK: - View Lifecycle 16 | 17 | override func viewDidLoad() { 18 | super.viewDidLoad() 19 | 20 | let button = UIButton(type: .system) 21 | button.setTitle("Tap Me!", for: []) 22 | button.translatesAutoresizingMaskIntoConstraints = false 23 | view.addSubview(button) 24 | button.heightAnchor.constraint(equalToConstant: 40).isActive = true 25 | button.widthAnchor.constraint(equalToConstant: 150).isActive = true 26 | button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true 27 | button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true 28 | button.addTarget(self, action: #selector(presentImagePickerSheet(gestureRecognizer:)), for: .touchUpInside) 29 | } 30 | 31 | // MARK: - Other Methods 32 | 33 | @objc func presentImagePickerSheet(gestureRecognizer: UITapGestureRecognizer) { 34 | let presentImagePickerController: (UIImagePickerController.SourceType) -> () = { source in 35 | let controller = UIImagePickerController() 36 | controller.delegate = self 37 | var sourceType = source 38 | if (!UIImagePickerController.isSourceTypeAvailable(sourceType)) { 39 | sourceType = .photoLibrary 40 | print("Fallback to camera roll as a source since the simulator doesn't support taking pictures") 41 | } 42 | controller.sourceType = sourceType 43 | 44 | self.present(controller, animated: true, completion: nil) 45 | } 46 | 47 | let controller = ImagePickerSheetController(mediaType: .imageAndVideo) 48 | controller.maximumSelection = 1 49 | controller.delegate = self 50 | 51 | controller.addAction(ImagePickerAction(title: NSLocalizedString("Take Photo Or Video", comment: "Action Title"), secondaryTitle: NSLocalizedString("Add comment", comment: "Action Title"), handler: { _ in 52 | presentImagePickerController(.camera) 53 | }, secondaryHandler: { _, numberOfPhotos in 54 | print("Comment \(numberOfPhotos) photos") 55 | })) 56 | controller.addAction(ImagePickerAction(title: NSLocalizedString("Photo Library", comment: "Action Title"), secondaryTitle: { NSString.localizedStringWithFormat(NSLocalizedString("ImagePickerSheet.button1.Send %lu Photo", comment: "Action Title") as NSString, $0) as String}, handler: { _ in 57 | presentImagePickerController(.photoLibrary) 58 | }, secondaryHandler: { _, numberOfPhotos in 59 | print("Send \(controller.selectedAssets)") 60 | })) 61 | controller.addAction(ImagePickerAction(cancelTitle: NSLocalizedString("Cancel", comment: "Action Title"))) 62 | 63 | if UIDevice.current.userInterfaceIdiom == .pad { 64 | controller.modalPresentationStyle = .popover 65 | controller.popoverPresentationController?.sourceView = view 66 | controller.popoverPresentationController?.sourceRect = CGRect(origin: view.center, size: CGSize()) 67 | } 68 | 69 | present(controller, animated: true, completion: nil) 70 | } 71 | 72 | } 73 | 74 | // MARK: - UIImagePickerControllerDelegate 75 | extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate { 76 | 77 | func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { 78 | dismiss(animated: true, completion: nil) 79 | } 80 | } 81 | 82 | // MARK: - ImagePickerSheetControllerDelegate 83 | extension ViewController: ImagePickerSheetControllerDelegate { 84 | 85 | func controllerWillEnlargePreview(_ controller: ImagePickerSheetController) { 86 | print("Will enlarge the preview") 87 | } 88 | 89 | func controllerDidEnlargePreview(_ controller: ImagePickerSheetController) { 90 | print("Did enlarge the preview") 91 | } 92 | 93 | func controller(_ controller: ImagePickerSheetController, willSelectAsset asset: PHAsset) { 94 | print("Will select an asset") 95 | } 96 | 97 | func controller(_ controller: ImagePickerSheetController, didSelectAsset asset: PHAsset) { 98 | print("Did select an asset") 99 | } 100 | 101 | func controller(_ controller: ImagePickerSheetController, willDeselectAsset asset: PHAsset) { 102 | print("Will deselect an asset") 103 | } 104 | 105 | func controller(_ controller: ImagePickerSheetController, didDeselectAsset asset: PHAsset) { 106 | print("Did deselect an asset") 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4944CA851B8DB7F3007EA349 /* SheetActionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4944CA841B8DB7F3007EA349 /* SheetActionCollectionViewCell.swift */; }; 11 | 494505AC1B2201AF00EF9ADC /* KIFExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494505AB1B2201AF00EF9ADC /* KIFExtensions.swift */; }; 12 | 494505AE1B22022F00EF9ADC /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 49A408461B21854300D6005C /* CoreGraphics.framework */; }; 13 | 494505B11B22026600EF9ADC /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 49A408481B2186D500D6005C /* IOKit.framework */; }; 14 | 494B3B711B8EF3CB004B467C /* ImagePickerSheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 494B3B701B8EF3CB004B467C /* ImagePickerSheetController.swift */; }; 15 | 495E5F551B9DE84D004729E6 /* KIF.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 49A408441B21853E00D6005C /* KIF.framework */; }; 16 | 495F89A91B394288006B9BA4 /* Nimble.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 49DC4AAD1B14FB9A00B4E78E /* Nimble.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 17 | 49665AD71B8F2DC6005AA666 /* SheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49665AD61B8F2DC6005AA666 /* SheetController.swift */; }; 18 | 4981CB031B8B246000A09750 /* SheetPreviewCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4981CAFD1B8B246000A09750 /* SheetPreviewCollectionViewCell.swift */; }; 19 | 4981CB041B8B246000A09750 /* PreviewCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4981CAFF1B8B246000A09750 /* PreviewCollectionViewCell.swift */; }; 20 | 4981CB051B8B246000A09750 /* PreviewCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4981CB001B8B246000A09750 /* PreviewCollectionView.swift */; }; 21 | 4981CB061B8B246000A09750 /* PreviewCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4981CB011B8B246000A09750 /* PreviewCollectionViewLayout.swift */; }; 22 | 4981CB071B8B246000A09750 /* PreviewSupplementaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4981CB021B8B246000A09750 /* PreviewSupplementaryView.swift */; }; 23 | 4981CB091B8B255A00A09750 /* SheetCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4981CB081B8B255A00A09750 /* SheetCollectionViewCell.swift */; }; 24 | 4989232F1B9B05CB00D68C8E /* KIF.framework in CopyFiles */ = {isa = PBXBuildFile; fileRef = 49A408441B21853E00D6005C /* KIF.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 25 | 499E65CF1B8E317C005B807F /* SheetCollectionViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 499E65CE1B8E317C005B807F /* SheetCollectionViewLayout.swift */; }; 26 | 49A93AE81B9CC0F100EE8A40 /* PresentationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49A93AE71B9CC0F100EE8A40 /* PresentationTests.swift */; }; 27 | 49A93AF11B9CC15A00EE8A40 /* DismissalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49A93AF01B9CC15A00EE8A40 /* DismissalTests.swift */; }; 28 | 49A93AF31B9CCB3900EE8A40 /* AddingActionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49A93AF21B9CCB3900EE8A40 /* AddingActionTests.swift */; }; 29 | 49A93AF51B9CCBA900EE8A40 /* ActionHandlingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49A93AF41B9CCBA900EE8A40 /* ActionHandlingTests.swift */; }; 30 | 49A93AF71B9CCC9500EE8A40 /* ImageSelectionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49A93AF61B9CCC9500EE8A40 /* ImageSelectionTests.swift */; }; 31 | 49DC4A571B14F1BC00B4E78E /* ImagePickerSheetController.h in Headers */ = {isa = PBXBuildFile; fileRef = 49DC4A561B14F1BC00B4E78E /* ImagePickerSheetController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 32 | 49DC4A5D1B14F1BC00B4E78E /* ImagePickerSheetController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 49DC4A511B14F1BC00B4E78E /* ImagePickerSheetController.framework */; }; 33 | 49DC4A641B14F1BC00B4E78E /* ImagePickerSheetControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DC4A631B14F1BC00B4E78E /* ImagePickerSheetControllerTests.swift */; }; 34 | 49DC4A751B14F1F600B4E78E /* AnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DC4A6D1B14F1F600B4E78E /* AnimationController.swift */; }; 35 | 49DC4A761B14F1F600B4E78E /* ImagePickerAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DC4A6E1B14F1F600B4E78E /* ImagePickerAction.swift */; }; 36 | 49DC4A7E1B14F22200B4E78E /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 49DC4A7D1B14F22200B4E78E /* Images.xcassets */; }; 37 | 49DC4A801B14F2EF00B4E78E /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 49DC4A7F1B14F2EF00B4E78E /* Photos.framework */; }; 38 | 49DC4A8B1B14F31500B4E78E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DC4A8A1B14F31500B4E78E /* AppDelegate.swift */; }; 39 | 49DC4A8D1B14F31500B4E78E /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49DC4A8C1B14F31500B4E78E /* ViewController.swift */; }; 40 | 49DC4A951B14F31500B4E78E /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 49DC4A931B14F31500B4E78E /* LaunchScreen.xib */; }; 41 | 49DC4AA81B14F3B800B4E78E /* ImagePickerSheetController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 49DC4A511B14F1BC00B4E78E /* ImagePickerSheetController.framework */; }; 42 | 49DC4AA91B14F3B800B4E78E /* ImagePickerSheetController.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 49DC4A511B14F1BC00B4E78E /* ImagePickerSheetController.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 43 | 49DC4AAF1B14FB9A00B4E78E /* Nimble.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 49DC4AAD1B14FB9A00B4E78E /* Nimble.framework */; }; 44 | 49F3118A1B15074100ACD6A7 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 49F3118C1B15074100ACD6A7 /* Localizable.stringsdict */; }; 45 | 49F3118D1B15074400ACD6A7 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 49F3118F1B15074400ACD6A7 /* Localizable.strings */; }; 46 | /* End PBXBuildFile section */ 47 | 48 | /* Begin PBXContainerItemProxy section */ 49 | 49DC4A5E1B14F1BC00B4E78E /* PBXContainerItemProxy */ = { 50 | isa = PBXContainerItemProxy; 51 | containerPortal = 49DC4A481B14F1BC00B4E78E /* Project object */; 52 | proxyType = 1; 53 | remoteGlobalIDString = 49DC4A501B14F1BC00B4E78E; 54 | remoteInfo = ImagePickerSheetController; 55 | }; 56 | 49DC4AAA1B14F3B800B4E78E /* PBXContainerItemProxy */ = { 57 | isa = PBXContainerItemProxy; 58 | containerPortal = 49DC4A481B14F1BC00B4E78E /* Project object */; 59 | proxyType = 1; 60 | remoteGlobalIDString = 49DC4A501B14F1BC00B4E78E; 61 | remoteInfo = ImagePickerSheetController; 62 | }; 63 | 49F3117A1B14FEFE00ACD6A7 /* PBXContainerItemProxy */ = { 64 | isa = PBXContainerItemProxy; 65 | containerPortal = 49DC4A481B14F1BC00B4E78E /* Project object */; 66 | proxyType = 1; 67 | remoteGlobalIDString = 49DC4A851B14F31500B4E78E; 68 | remoteInfo = Example; 69 | }; 70 | /* End PBXContainerItemProxy section */ 71 | 72 | /* Begin PBXCopyFilesBuildPhase section */ 73 | 495F89A61B394277006B9BA4 /* CopyFiles */ = { 74 | isa = PBXCopyFilesBuildPhase; 75 | buildActionMask = 2147483647; 76 | dstPath = ""; 77 | dstSubfolderSpec = 10; 78 | files = ( 79 | 4989232F1B9B05CB00D68C8E /* KIF.framework in CopyFiles */, 80 | 495F89A91B394288006B9BA4 /* Nimble.framework in CopyFiles */, 81 | ); 82 | runOnlyForDeploymentPostprocessing = 0; 83 | }; 84 | 49DC4AAC1B14F3B800B4E78E /* Embed Frameworks */ = { 85 | isa = PBXCopyFilesBuildPhase; 86 | buildActionMask = 2147483647; 87 | dstPath = ""; 88 | dstSubfolderSpec = 10; 89 | files = ( 90 | 49DC4AA91B14F3B800B4E78E /* ImagePickerSheetController.framework in Embed Frameworks */, 91 | ); 92 | name = "Embed Frameworks"; 93 | runOnlyForDeploymentPostprocessing = 0; 94 | }; 95 | /* End PBXCopyFilesBuildPhase section */ 96 | 97 | /* Begin PBXFileReference section */ 98 | 4944CA841B8DB7F3007EA349 /* SheetActionCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SheetActionCollectionViewCell.swift; sourceTree = ""; }; 99 | 494505AB1B2201AF00EF9ADC /* KIFExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KIFExtensions.swift; sourceTree = ""; }; 100 | 494B3B701B8EF3CB004B467C /* ImagePickerSheetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerSheetController.swift; sourceTree = ""; }; 101 | 49665AD61B8F2DC6005AA666 /* SheetController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SheetController.swift; sourceTree = ""; }; 102 | 496DC6D11B86F6E90011AF73 /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; 103 | 4981CAFD1B8B246000A09750 /* SheetPreviewCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; path = SheetPreviewCollectionViewCell.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 104 | 4981CAFF1B8B246000A09750 /* PreviewCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewCollectionViewCell.swift; sourceTree = ""; }; 105 | 4981CB001B8B246000A09750 /* PreviewCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewCollectionView.swift; sourceTree = ""; }; 106 | 4981CB011B8B246000A09750 /* PreviewCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewCollectionViewLayout.swift; sourceTree = ""; }; 107 | 4981CB021B8B246000A09750 /* PreviewSupplementaryView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewSupplementaryView.swift; sourceTree = ""; }; 108 | 4981CB081B8B255A00A09750 /* SheetCollectionViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SheetCollectionViewCell.swift; sourceTree = ""; }; 109 | 499E65CE1B8E317C005B807F /* SheetCollectionViewLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SheetCollectionViewLayout.swift; sourceTree = ""; }; 110 | 49A408441B21853E00D6005C /* KIF.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = KIF.framework; path = ../Carthage/Build/iOS/KIF.framework; sourceTree = ""; }; 111 | 49A408461B21854300D6005C /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; 112 | 49A408481B2186D500D6005C /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk/System/Library/Frameworks/IOKit.framework; sourceTree = DEVELOPER_DIR; }; 113 | 49A93AE71B9CC0F100EE8A40 /* PresentationTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PresentationTests.swift; sourceTree = ""; }; 114 | 49A93AF01B9CC15A00EE8A40 /* DismissalTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissalTests.swift; sourceTree = ""; }; 115 | 49A93AF21B9CCB3900EE8A40 /* AddingActionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddingActionTests.swift; sourceTree = ""; }; 116 | 49A93AF41B9CCBA900EE8A40 /* ActionHandlingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionHandlingTests.swift; sourceTree = ""; }; 117 | 49A93AF61B9CCC9500EE8A40 /* ImageSelectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageSelectionTests.swift; sourceTree = ""; }; 118 | 49DC4A511B14F1BC00B4E78E /* ImagePickerSheetController.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ImagePickerSheetController.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 119 | 49DC4A551B14F1BC00B4E78E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 120 | 49DC4A561B14F1BC00B4E78E /* ImagePickerSheetController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ImagePickerSheetController.h; sourceTree = ""; }; 121 | 49DC4A5C1B14F1BC00B4E78E /* ImagePickerSheetControllerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ImagePickerSheetControllerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 122 | 49DC4A621B14F1BC00B4E78E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 123 | 49DC4A631B14F1BC00B4E78E /* ImagePickerSheetControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerSheetControllerTests.swift; sourceTree = ""; }; 124 | 49DC4A6D1B14F1F600B4E78E /* AnimationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnimationController.swift; sourceTree = ""; }; 125 | 49DC4A6E1B14F1F600B4E78E /* ImagePickerAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImagePickerAction.swift; sourceTree = ""; }; 126 | 49DC4A7D1B14F22200B4E78E /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 127 | 49DC4A7F1B14F2EF00B4E78E /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; 128 | 49DC4A861B14F31500B4E78E /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 129 | 49DC4A891B14F31500B4E78E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 130 | 49DC4A8A1B14F31500B4E78E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 131 | 49DC4A8C1B14F31500B4E78E /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 132 | 49DC4A941B14F31500B4E78E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 133 | 49DC4AAD1B14FB9A00B4E78E /* Nimble.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Nimble.framework; path = ../Carthage/Build/iOS/Nimble.framework; sourceTree = ""; }; 134 | 49F3118B1B15074100ACD6A7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.xml; name = Base; path = Base.lproj/Localizable.stringsdict; sourceTree = ""; }; 135 | 49F3118E1B15074400ACD6A7 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; 136 | /* End PBXFileReference section */ 137 | 138 | /* Begin PBXFrameworksBuildPhase section */ 139 | 49DC4A4D1B14F1BC00B4E78E /* Frameworks */ = { 140 | isa = PBXFrameworksBuildPhase; 141 | buildActionMask = 2147483647; 142 | files = ( 143 | 49DC4A801B14F2EF00B4E78E /* Photos.framework in Frameworks */, 144 | ); 145 | runOnlyForDeploymentPostprocessing = 0; 146 | }; 147 | 49DC4A591B14F1BC00B4E78E /* Frameworks */ = { 148 | isa = PBXFrameworksBuildPhase; 149 | buildActionMask = 2147483647; 150 | files = ( 151 | 495E5F551B9DE84D004729E6 /* KIF.framework in Frameworks */, 152 | 49DC4A5D1B14F1BC00B4E78E /* ImagePickerSheetController.framework in Frameworks */, 153 | 494505AE1B22022F00EF9ADC /* CoreGraphics.framework in Frameworks */, 154 | 494505B11B22026600EF9ADC /* IOKit.framework in Frameworks */, 155 | 49DC4AAF1B14FB9A00B4E78E /* Nimble.framework in Frameworks */, 156 | ); 157 | runOnlyForDeploymentPostprocessing = 0; 158 | }; 159 | 49DC4A831B14F31500B4E78E /* Frameworks */ = { 160 | isa = PBXFrameworksBuildPhase; 161 | buildActionMask = 2147483647; 162 | files = ( 163 | 49DC4AA81B14F3B800B4E78E /* ImagePickerSheetController.framework in Frameworks */, 164 | ); 165 | runOnlyForDeploymentPostprocessing = 0; 166 | }; 167 | /* End PBXFrameworksBuildPhase section */ 168 | 169 | /* Begin PBXGroup section */ 170 | 4981CAFC1B8B246000A09750 /* Sheet */ = { 171 | isa = PBXGroup; 172 | children = ( 173 | 49665AD61B8F2DC6005AA666 /* SheetController.swift */, 174 | 499E65CE1B8E317C005B807F /* SheetCollectionViewLayout.swift */, 175 | 4981CB081B8B255A00A09750 /* SheetCollectionViewCell.swift */, 176 | 4981CAFD1B8B246000A09750 /* SheetPreviewCollectionViewCell.swift */, 177 | 4944CA841B8DB7F3007EA349 /* SheetActionCollectionViewCell.swift */, 178 | 4981CAFE1B8B246000A09750 /* Preview */, 179 | ); 180 | path = Sheet; 181 | sourceTree = ""; 182 | }; 183 | 4981CAFE1B8B246000A09750 /* Preview */ = { 184 | isa = PBXGroup; 185 | children = ( 186 | 4981CB001B8B246000A09750 /* PreviewCollectionView.swift */, 187 | 4981CB011B8B246000A09750 /* PreviewCollectionViewLayout.swift */, 188 | 4981CAFF1B8B246000A09750 /* PreviewCollectionViewCell.swift */, 189 | 4981CB021B8B246000A09750 /* PreviewSupplementaryView.swift */, 190 | ); 191 | path = Preview; 192 | sourceTree = ""; 193 | }; 194 | 49DC4A471B14F1BC00B4E78E = { 195 | isa = PBXGroup; 196 | children = ( 197 | 49DC4A531B14F1BC00B4E78E /* ImagePickerSheetController */, 198 | 49DC4A601B14F1BC00B4E78E /* ImagePickerSheetControllerTests */, 199 | 49DC4A871B14F31500B4E78E /* Example */, 200 | 49DC4AB11B14FBAD00B4E78E /* Frameworks */, 201 | 49DC4A521B14F1BC00B4E78E /* Products */, 202 | ); 203 | sourceTree = ""; 204 | }; 205 | 49DC4A521B14F1BC00B4E78E /* Products */ = { 206 | isa = PBXGroup; 207 | children = ( 208 | 49DC4A511B14F1BC00B4E78E /* ImagePickerSheetController.framework */, 209 | 49DC4A5C1B14F1BC00B4E78E /* ImagePickerSheetControllerTests.xctest */, 210 | 49DC4A861B14F31500B4E78E /* Example.app */, 211 | ); 212 | name = Products; 213 | sourceTree = ""; 214 | }; 215 | 49DC4A531B14F1BC00B4E78E /* ImagePickerSheetController */ = { 216 | isa = PBXGroup; 217 | children = ( 218 | 49DC4A6E1B14F1F600B4E78E /* ImagePickerAction.swift */, 219 | 49DC4A6D1B14F1F600B4E78E /* AnimationController.swift */, 220 | 494B3B701B8EF3CB004B467C /* ImagePickerSheetController.swift */, 221 | 4981CAFC1B8B246000A09750 /* Sheet */, 222 | 49DC4A7D1B14F22200B4E78E /* Images.xcassets */, 223 | 49DC4A541B14F1BC00B4E78E /* Supporting Files */, 224 | ); 225 | path = ImagePickerSheetController; 226 | sourceTree = ""; 227 | }; 228 | 49DC4A541B14F1BC00B4E78E /* Supporting Files */ = { 229 | isa = PBXGroup; 230 | children = ( 231 | 49DC4A561B14F1BC00B4E78E /* ImagePickerSheetController.h */, 232 | 49DC4A551B14F1BC00B4E78E /* Info.plist */, 233 | ); 234 | name = "Supporting Files"; 235 | sourceTree = ""; 236 | }; 237 | 49DC4A601B14F1BC00B4E78E /* ImagePickerSheetControllerTests */ = { 238 | isa = PBXGroup; 239 | children = ( 240 | 49A93AE71B9CC0F100EE8A40 /* PresentationTests.swift */, 241 | 49A93AF01B9CC15A00EE8A40 /* DismissalTests.swift */, 242 | 49A93AF21B9CCB3900EE8A40 /* AddingActionTests.swift */, 243 | 49A93AF41B9CCBA900EE8A40 /* ActionHandlingTests.swift */, 244 | 49A93AF61B9CCC9500EE8A40 /* ImageSelectionTests.swift */, 245 | 49DC4A631B14F1BC00B4E78E /* ImagePickerSheetControllerTests.swift */, 246 | 494505AB1B2201AF00EF9ADC /* KIFExtensions.swift */, 247 | 49DC4A611B14F1BC00B4E78E /* Supporting Files */, 248 | ); 249 | path = ImagePickerSheetControllerTests; 250 | sourceTree = ""; 251 | }; 252 | 49DC4A611B14F1BC00B4E78E /* Supporting Files */ = { 253 | isa = PBXGroup; 254 | children = ( 255 | 49DC4A621B14F1BC00B4E78E /* Info.plist */, 256 | ); 257 | name = "Supporting Files"; 258 | sourceTree = ""; 259 | }; 260 | 49DC4A871B14F31500B4E78E /* Example */ = { 261 | isa = PBXGroup; 262 | children = ( 263 | 49DC4A8A1B14F31500B4E78E /* AppDelegate.swift */, 264 | 49DC4A8C1B14F31500B4E78E /* ViewController.swift */, 265 | 49DC4A881B14F31500B4E78E /* Supporting Files */, 266 | ); 267 | path = Example; 268 | sourceTree = ""; 269 | }; 270 | 49DC4A881B14F31500B4E78E /* Supporting Files */ = { 271 | isa = PBXGroup; 272 | children = ( 273 | 49F3118F1B15074400ACD6A7 /* Localizable.strings */, 274 | 49F3118C1B15074100ACD6A7 /* Localizable.stringsdict */, 275 | 49DC4A931B14F31500B4E78E /* LaunchScreen.xib */, 276 | 49DC4A891B14F31500B4E78E /* Info.plist */, 277 | ); 278 | name = "Supporting Files"; 279 | sourceTree = ""; 280 | }; 281 | 49DC4AB11B14FBAD00B4E78E /* Frameworks */ = { 282 | isa = PBXGroup; 283 | children = ( 284 | 496DC6D11B86F6E90011AF73 /* CoreMedia.framework */, 285 | 49A408481B2186D500D6005C /* IOKit.framework */, 286 | 49A408461B21854300D6005C /* CoreGraphics.framework */, 287 | 49A408441B21853E00D6005C /* KIF.framework */, 288 | 49DC4A7F1B14F2EF00B4E78E /* Photos.framework */, 289 | 49DC4AAD1B14FB9A00B4E78E /* Nimble.framework */, 290 | ); 291 | name = Frameworks; 292 | sourceTree = ""; 293 | }; 294 | /* End PBXGroup section */ 295 | 296 | /* Begin PBXHeadersBuildPhase section */ 297 | 49DC4A4E1B14F1BC00B4E78E /* Headers */ = { 298 | isa = PBXHeadersBuildPhase; 299 | buildActionMask = 2147483647; 300 | files = ( 301 | 49DC4A571B14F1BC00B4E78E /* ImagePickerSheetController.h in Headers */, 302 | ); 303 | runOnlyForDeploymentPostprocessing = 0; 304 | }; 305 | /* End PBXHeadersBuildPhase section */ 306 | 307 | /* Begin PBXNativeTarget section */ 308 | 49DC4A501B14F1BC00B4E78E /* ImagePickerSheetController */ = { 309 | isa = PBXNativeTarget; 310 | buildConfigurationList = 49DC4A671B14F1BC00B4E78E /* Build configuration list for PBXNativeTarget "ImagePickerSheetController" */; 311 | buildPhases = ( 312 | 49DC4A4C1B14F1BC00B4E78E /* Sources */, 313 | 49DC4A4D1B14F1BC00B4E78E /* Frameworks */, 314 | 49DC4A4E1B14F1BC00B4E78E /* Headers */, 315 | 49DC4A4F1B14F1BC00B4E78E /* Resources */, 316 | ); 317 | buildRules = ( 318 | ); 319 | dependencies = ( 320 | ); 321 | name = ImagePickerSheetController; 322 | productName = ImagePickerSheetController; 323 | productReference = 49DC4A511B14F1BC00B4E78E /* ImagePickerSheetController.framework */; 324 | productType = "com.apple.product-type.framework"; 325 | }; 326 | 49DC4A5B1B14F1BC00B4E78E /* ImagePickerSheetControllerTests */ = { 327 | isa = PBXNativeTarget; 328 | buildConfigurationList = 49DC4A6A1B14F1BC00B4E78E /* Build configuration list for PBXNativeTarget "ImagePickerSheetControllerTests" */; 329 | buildPhases = ( 330 | 49DC4A581B14F1BC00B4E78E /* Sources */, 331 | 49DC4A591B14F1BC00B4E78E /* Frameworks */, 332 | 49DC4A5A1B14F1BC00B4E78E /* Resources */, 333 | 495F89A61B394277006B9BA4 /* CopyFiles */, 334 | ); 335 | buildRules = ( 336 | ); 337 | dependencies = ( 338 | 49DC4A5F1B14F1BC00B4E78E /* PBXTargetDependency */, 339 | 49F3117B1B14FEFE00ACD6A7 /* PBXTargetDependency */, 340 | ); 341 | name = ImagePickerSheetControllerTests; 342 | productName = ImagePickerSheetControllerTests; 343 | productReference = 49DC4A5C1B14F1BC00B4E78E /* ImagePickerSheetControllerTests.xctest */; 344 | productType = "com.apple.product-type.bundle.unit-test"; 345 | }; 346 | 49DC4A851B14F31500B4E78E /* Example */ = { 347 | isa = PBXNativeTarget; 348 | buildConfigurationList = 49DC4AA21B14F31500B4E78E /* Build configuration list for PBXNativeTarget "Example" */; 349 | buildPhases = ( 350 | 49DC4A821B14F31500B4E78E /* Sources */, 351 | 49DC4A831B14F31500B4E78E /* Frameworks */, 352 | 49DC4A841B14F31500B4E78E /* Resources */, 353 | 49DC4AAC1B14F3B800B4E78E /* Embed Frameworks */, 354 | ); 355 | buildRules = ( 356 | ); 357 | dependencies = ( 358 | 49DC4AAB1B14F3B800B4E78E /* PBXTargetDependency */, 359 | ); 360 | name = Example; 361 | productName = Example; 362 | productReference = 49DC4A861B14F31500B4E78E /* Example.app */; 363 | productType = "com.apple.product-type.application"; 364 | }; 365 | /* End PBXNativeTarget section */ 366 | 367 | /* Begin PBXProject section */ 368 | 49DC4A481B14F1BC00B4E78E /* Project object */ = { 369 | isa = PBXProject; 370 | attributes = { 371 | LastSwiftUpdateCheck = 0700; 372 | LastUpgradeCheck = 1010; 373 | ORGANIZATIONNAME = "Laurin Brandner"; 374 | TargetAttributes = { 375 | 49DC4A501B14F1BC00B4E78E = { 376 | CreatedOnToolsVersion = 6.3.1; 377 | LastSwiftMigration = 0800; 378 | }; 379 | 49DC4A5B1B14F1BC00B4E78E = { 380 | CreatedOnToolsVersion = 6.3.1; 381 | LastSwiftMigration = 0800; 382 | TestTargetID = 49DC4A851B14F31500B4E78E; 383 | }; 384 | 49DC4A851B14F31500B4E78E = { 385 | CreatedOnToolsVersion = 6.3.1; 386 | DevelopmentTeam = 5953RHWYWT; 387 | LastSwiftMigration = 0800; 388 | }; 389 | }; 390 | }; 391 | buildConfigurationList = 49DC4A4B1B14F1BC00B4E78E /* Build configuration list for PBXProject "ImagePickerSheetController" */; 392 | compatibilityVersion = "Xcode 3.2"; 393 | developmentRegion = English; 394 | hasScannedForEncodings = 0; 395 | knownRegions = ( 396 | en, 397 | Base, 398 | ); 399 | mainGroup = 49DC4A471B14F1BC00B4E78E; 400 | productRefGroup = 49DC4A521B14F1BC00B4E78E /* Products */; 401 | projectDirPath = ""; 402 | projectRoot = ""; 403 | targets = ( 404 | 49DC4A501B14F1BC00B4E78E /* ImagePickerSheetController */, 405 | 49DC4A5B1B14F1BC00B4E78E /* ImagePickerSheetControllerTests */, 406 | 49DC4A851B14F31500B4E78E /* Example */, 407 | ); 408 | }; 409 | /* End PBXProject section */ 410 | 411 | /* Begin PBXResourcesBuildPhase section */ 412 | 49DC4A4F1B14F1BC00B4E78E /* Resources */ = { 413 | isa = PBXResourcesBuildPhase; 414 | buildActionMask = 2147483647; 415 | files = ( 416 | 49DC4A7E1B14F22200B4E78E /* Images.xcassets in Resources */, 417 | ); 418 | runOnlyForDeploymentPostprocessing = 0; 419 | }; 420 | 49DC4A5A1B14F1BC00B4E78E /* Resources */ = { 421 | isa = PBXResourcesBuildPhase; 422 | buildActionMask = 2147483647; 423 | files = ( 424 | ); 425 | runOnlyForDeploymentPostprocessing = 0; 426 | }; 427 | 49DC4A841B14F31500B4E78E /* Resources */ = { 428 | isa = PBXResourcesBuildPhase; 429 | buildActionMask = 2147483647; 430 | files = ( 431 | 49DC4A951B14F31500B4E78E /* LaunchScreen.xib in Resources */, 432 | 49F3118D1B15074400ACD6A7 /* Localizable.strings in Resources */, 433 | 49F3118A1B15074100ACD6A7 /* Localizable.stringsdict in Resources */, 434 | ); 435 | runOnlyForDeploymentPostprocessing = 0; 436 | }; 437 | /* End PBXResourcesBuildPhase section */ 438 | 439 | /* Begin PBXSourcesBuildPhase section */ 440 | 49DC4A4C1B14F1BC00B4E78E /* Sources */ = { 441 | isa = PBXSourcesBuildPhase; 442 | buildActionMask = 2147483647; 443 | files = ( 444 | 4981CB041B8B246000A09750 /* PreviewCollectionViewCell.swift in Sources */, 445 | 49DC4A751B14F1F600B4E78E /* AnimationController.swift in Sources */, 446 | 4981CB061B8B246000A09750 /* PreviewCollectionViewLayout.swift in Sources */, 447 | 49DC4A761B14F1F600B4E78E /* ImagePickerAction.swift in Sources */, 448 | 494B3B711B8EF3CB004B467C /* ImagePickerSheetController.swift in Sources */, 449 | 49665AD71B8F2DC6005AA666 /* SheetController.swift in Sources */, 450 | 499E65CF1B8E317C005B807F /* SheetCollectionViewLayout.swift in Sources */, 451 | 4981CB031B8B246000A09750 /* SheetPreviewCollectionViewCell.swift in Sources */, 452 | 4981CB091B8B255A00A09750 /* SheetCollectionViewCell.swift in Sources */, 453 | 4981CB071B8B246000A09750 /* PreviewSupplementaryView.swift in Sources */, 454 | 4981CB051B8B246000A09750 /* PreviewCollectionView.swift in Sources */, 455 | 4944CA851B8DB7F3007EA349 /* SheetActionCollectionViewCell.swift in Sources */, 456 | ); 457 | runOnlyForDeploymentPostprocessing = 0; 458 | }; 459 | 49DC4A581B14F1BC00B4E78E /* Sources */ = { 460 | isa = PBXSourcesBuildPhase; 461 | buildActionMask = 2147483647; 462 | files = ( 463 | 494505AC1B2201AF00EF9ADC /* KIFExtensions.swift in Sources */, 464 | 49A93AE81B9CC0F100EE8A40 /* PresentationTests.swift in Sources */, 465 | 49A93AF51B9CCBA900EE8A40 /* ActionHandlingTests.swift in Sources */, 466 | 49DC4A641B14F1BC00B4E78E /* ImagePickerSheetControllerTests.swift in Sources */, 467 | 49A93AF11B9CC15A00EE8A40 /* DismissalTests.swift in Sources */, 468 | 49A93AF31B9CCB3900EE8A40 /* AddingActionTests.swift in Sources */, 469 | 49A93AF71B9CCC9500EE8A40 /* ImageSelectionTests.swift in Sources */, 470 | ); 471 | runOnlyForDeploymentPostprocessing = 0; 472 | }; 473 | 49DC4A821B14F31500B4E78E /* Sources */ = { 474 | isa = PBXSourcesBuildPhase; 475 | buildActionMask = 2147483647; 476 | files = ( 477 | 49DC4A8D1B14F31500B4E78E /* ViewController.swift in Sources */, 478 | 49DC4A8B1B14F31500B4E78E /* AppDelegate.swift in Sources */, 479 | ); 480 | runOnlyForDeploymentPostprocessing = 0; 481 | }; 482 | /* End PBXSourcesBuildPhase section */ 483 | 484 | /* Begin PBXTargetDependency section */ 485 | 49DC4A5F1B14F1BC00B4E78E /* PBXTargetDependency */ = { 486 | isa = PBXTargetDependency; 487 | target = 49DC4A501B14F1BC00B4E78E /* ImagePickerSheetController */; 488 | targetProxy = 49DC4A5E1B14F1BC00B4E78E /* PBXContainerItemProxy */; 489 | }; 490 | 49DC4AAB1B14F3B800B4E78E /* PBXTargetDependency */ = { 491 | isa = PBXTargetDependency; 492 | target = 49DC4A501B14F1BC00B4E78E /* ImagePickerSheetController */; 493 | targetProxy = 49DC4AAA1B14F3B800B4E78E /* PBXContainerItemProxy */; 494 | }; 495 | 49F3117B1B14FEFE00ACD6A7 /* PBXTargetDependency */ = { 496 | isa = PBXTargetDependency; 497 | target = 49DC4A851B14F31500B4E78E /* Example */; 498 | targetProxy = 49F3117A1B14FEFE00ACD6A7 /* PBXContainerItemProxy */; 499 | }; 500 | /* End PBXTargetDependency section */ 501 | 502 | /* Begin PBXVariantGroup section */ 503 | 49DC4A931B14F31500B4E78E /* LaunchScreen.xib */ = { 504 | isa = PBXVariantGroup; 505 | children = ( 506 | 49DC4A941B14F31500B4E78E /* Base */, 507 | ); 508 | name = LaunchScreen.xib; 509 | sourceTree = ""; 510 | }; 511 | 49F3118C1B15074100ACD6A7 /* Localizable.stringsdict */ = { 512 | isa = PBXVariantGroup; 513 | children = ( 514 | 49F3118B1B15074100ACD6A7 /* Base */, 515 | ); 516 | name = Localizable.stringsdict; 517 | sourceTree = ""; 518 | }; 519 | 49F3118F1B15074400ACD6A7 /* Localizable.strings */ = { 520 | isa = PBXVariantGroup; 521 | children = ( 522 | 49F3118E1B15074400ACD6A7 /* Base */, 523 | ); 524 | name = Localizable.strings; 525 | sourceTree = ""; 526 | }; 527 | /* End PBXVariantGroup section */ 528 | 529 | /* Begin XCBuildConfiguration section */ 530 | 49DC4A651B14F1BC00B4E78E /* Debug */ = { 531 | isa = XCBuildConfiguration; 532 | buildSettings = { 533 | ALWAYS_SEARCH_USER_PATHS = NO; 534 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 535 | CLANG_CXX_LIBRARY = "libc++"; 536 | CLANG_ENABLE_MODULES = YES; 537 | CLANG_ENABLE_OBJC_ARC = YES; 538 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 539 | CLANG_WARN_BOOL_CONVERSION = YES; 540 | CLANG_WARN_COMMA = YES; 541 | CLANG_WARN_CONSTANT_CONVERSION = YES; 542 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 543 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 544 | CLANG_WARN_EMPTY_BODY = YES; 545 | CLANG_WARN_ENUM_CONVERSION = YES; 546 | CLANG_WARN_INFINITE_RECURSION = YES; 547 | CLANG_WARN_INT_CONVERSION = YES; 548 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 549 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 550 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 551 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 552 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 553 | CLANG_WARN_STRICT_PROTOTYPES = YES; 554 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 555 | CLANG_WARN_UNREACHABLE_CODE = YES; 556 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 557 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 558 | COPY_PHASE_STRIP = NO; 559 | CURRENT_PROJECT_VERSION = 1; 560 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 561 | ENABLE_STRICT_OBJC_MSGSEND = YES; 562 | ENABLE_TESTABILITY = YES; 563 | GCC_C_LANGUAGE_STANDARD = gnu99; 564 | GCC_DYNAMIC_NO_PIC = NO; 565 | GCC_NO_COMMON_BLOCKS = YES; 566 | GCC_OPTIMIZATION_LEVEL = 0; 567 | GCC_PREPROCESSOR_DEFINITIONS = ( 568 | "DEBUG=1", 569 | "$(inherited)", 570 | ); 571 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 572 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 573 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 574 | GCC_WARN_UNDECLARED_SELECTOR = YES; 575 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 576 | GCC_WARN_UNUSED_FUNCTION = YES; 577 | GCC_WARN_UNUSED_VARIABLE = YES; 578 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 579 | MTL_ENABLE_DEBUG_INFO = YES; 580 | ONLY_ACTIVE_ARCH = YES; 581 | SDKROOT = iphoneos; 582 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 583 | SWIFT_VERSION = 4.2; 584 | TARGETED_DEVICE_FAMILY = "1,2"; 585 | VERSIONING_SYSTEM = "apple-generic"; 586 | VERSION_INFO_PREFIX = ""; 587 | }; 588 | name = Debug; 589 | }; 590 | 49DC4A661B14F1BC00B4E78E /* Release */ = { 591 | isa = XCBuildConfiguration; 592 | buildSettings = { 593 | ALWAYS_SEARCH_USER_PATHS = NO; 594 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 595 | CLANG_CXX_LIBRARY = "libc++"; 596 | CLANG_ENABLE_MODULES = YES; 597 | CLANG_ENABLE_OBJC_ARC = YES; 598 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 599 | CLANG_WARN_BOOL_CONVERSION = YES; 600 | CLANG_WARN_COMMA = YES; 601 | CLANG_WARN_CONSTANT_CONVERSION = YES; 602 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 603 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 604 | CLANG_WARN_EMPTY_BODY = YES; 605 | CLANG_WARN_ENUM_CONVERSION = YES; 606 | CLANG_WARN_INFINITE_RECURSION = YES; 607 | CLANG_WARN_INT_CONVERSION = YES; 608 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 609 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 610 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 611 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 612 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 613 | CLANG_WARN_STRICT_PROTOTYPES = YES; 614 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 615 | CLANG_WARN_UNREACHABLE_CODE = YES; 616 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 617 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 618 | COPY_PHASE_STRIP = NO; 619 | CURRENT_PROJECT_VERSION = 1; 620 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 621 | ENABLE_NS_ASSERTIONS = NO; 622 | ENABLE_STRICT_OBJC_MSGSEND = YES; 623 | GCC_C_LANGUAGE_STANDARD = gnu99; 624 | GCC_NO_COMMON_BLOCKS = YES; 625 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 626 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 627 | GCC_WARN_UNDECLARED_SELECTOR = YES; 628 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 629 | GCC_WARN_UNUSED_FUNCTION = YES; 630 | GCC_WARN_UNUSED_VARIABLE = YES; 631 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 632 | MTL_ENABLE_DEBUG_INFO = NO; 633 | SDKROOT = iphoneos; 634 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 635 | SWIFT_VERSION = 4.2; 636 | TARGETED_DEVICE_FAMILY = "1,2"; 637 | VALIDATE_PRODUCT = YES; 638 | VERSIONING_SYSTEM = "apple-generic"; 639 | VERSION_INFO_PREFIX = ""; 640 | }; 641 | name = Release; 642 | }; 643 | 49DC4A681B14F1BC00B4E78E /* Debug */ = { 644 | isa = XCBuildConfiguration; 645 | buildSettings = { 646 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 647 | DEFINES_MODULE = YES; 648 | DYLIB_COMPATIBILITY_VERSION = 1; 649 | DYLIB_CURRENT_VERSION = 1; 650 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 651 | FRAMEWORK_SEARCH_PATHS = ""; 652 | INFOPLIST_FILE = ImagePickerSheetController/Info.plist; 653 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 654 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 655 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 656 | PRODUCT_BUNDLE_IDENTIFIER = "ch.laurinbrandner.$(PRODUCT_NAME:rfc1034identifier)"; 657 | PRODUCT_NAME = "$(TARGET_NAME)"; 658 | SKIP_INSTALL = YES; 659 | SWIFT_VERSION = 4.2; 660 | }; 661 | name = Debug; 662 | }; 663 | 49DC4A691B14F1BC00B4E78E /* Release */ = { 664 | isa = XCBuildConfiguration; 665 | buildSettings = { 666 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; 667 | DEFINES_MODULE = YES; 668 | DYLIB_COMPATIBILITY_VERSION = 1; 669 | DYLIB_CURRENT_VERSION = 1; 670 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 671 | FRAMEWORK_SEARCH_PATHS = ""; 672 | INFOPLIST_FILE = ImagePickerSheetController/Info.plist; 673 | INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; 674 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 675 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 676 | PRODUCT_BUNDLE_IDENTIFIER = "ch.laurinbrandner.$(PRODUCT_NAME:rfc1034identifier)"; 677 | PRODUCT_NAME = "$(TARGET_NAME)"; 678 | SKIP_INSTALL = YES; 679 | SWIFT_VERSION = 4.2; 680 | }; 681 | name = Release; 682 | }; 683 | 49DC4A6B1B14F1BC00B4E78E /* Debug */ = { 684 | isa = XCBuildConfiguration; 685 | buildSettings = { 686 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 687 | DEVELOPMENT_TEAM = ""; 688 | FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../Carthage/Build/iOS"; 689 | GCC_PREPROCESSOR_DEFINITIONS = ( 690 | "DEBUG=1", 691 | "$(inherited)", 692 | ); 693 | INFOPLIST_FILE = ImagePickerSheetControllerTests/Info.plist; 694 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 695 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 696 | PRODUCT_BUNDLE_IDENTIFIER = "ch.laurinbrandner.$(PRODUCT_NAME:rfc1034identifier)"; 697 | PRODUCT_NAME = "$(TARGET_NAME)"; 698 | SWIFT_VERSION = 3.0; 699 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; 700 | }; 701 | name = Debug; 702 | }; 703 | 49DC4A6C1B14F1BC00B4E78E /* Release */ = { 704 | isa = XCBuildConfiguration; 705 | buildSettings = { 706 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 707 | DEVELOPMENT_TEAM = ""; 708 | FRAMEWORK_SEARCH_PATHS = "$(PROJECT_DIR)/../Carthage/Build/iOS"; 709 | INFOPLIST_FILE = ImagePickerSheetControllerTests/Info.plist; 710 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 711 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 712 | PRODUCT_BUNDLE_IDENTIFIER = "ch.laurinbrandner.$(PRODUCT_NAME:rfc1034identifier)"; 713 | PRODUCT_NAME = "$(TARGET_NAME)"; 714 | SWIFT_VERSION = 3.0; 715 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Example.app/Example"; 716 | }; 717 | name = Release; 718 | }; 719 | 49DC4AA31B14F31500B4E78E /* Debug */ = { 720 | isa = XCBuildConfiguration; 721 | buildSettings = { 722 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 723 | CODE_SIGN_IDENTITY = "iPhone Developer"; 724 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 725 | GCC_PREPROCESSOR_DEFINITIONS = ( 726 | "DEBUG=1", 727 | "$(inherited)", 728 | ); 729 | INFOPLIST_FILE = Example/Info.plist; 730 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 731 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 732 | PRODUCT_BUNDLE_IDENTIFIER = "ch.laurinbrandner.$(PRODUCT_NAME:rfc1034identifier)"; 733 | PRODUCT_NAME = "$(TARGET_NAME)"; 734 | PROVISIONING_PROFILE = ""; 735 | SWIFT_VERSION = 4.2; 736 | }; 737 | name = Debug; 738 | }; 739 | 49DC4AA41B14F31500B4E78E /* Release */ = { 740 | isa = XCBuildConfiguration; 741 | buildSettings = { 742 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 743 | CODE_SIGN_IDENTITY = "iPhone Developer"; 744 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 745 | INFOPLIST_FILE = Example/Info.plist; 746 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 747 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 748 | PRODUCT_BUNDLE_IDENTIFIER = "ch.laurinbrandner.$(PRODUCT_NAME:rfc1034identifier)"; 749 | PRODUCT_NAME = "$(TARGET_NAME)"; 750 | PROVISIONING_PROFILE = ""; 751 | SWIFT_VERSION = 4.2; 752 | }; 753 | name = Release; 754 | }; 755 | /* End XCBuildConfiguration section */ 756 | 757 | /* Begin XCConfigurationList section */ 758 | 49DC4A4B1B14F1BC00B4E78E /* Build configuration list for PBXProject "ImagePickerSheetController" */ = { 759 | isa = XCConfigurationList; 760 | buildConfigurations = ( 761 | 49DC4A651B14F1BC00B4E78E /* Debug */, 762 | 49DC4A661B14F1BC00B4E78E /* Release */, 763 | ); 764 | defaultConfigurationIsVisible = 0; 765 | defaultConfigurationName = Release; 766 | }; 767 | 49DC4A671B14F1BC00B4E78E /* Build configuration list for PBXNativeTarget "ImagePickerSheetController" */ = { 768 | isa = XCConfigurationList; 769 | buildConfigurations = ( 770 | 49DC4A681B14F1BC00B4E78E /* Debug */, 771 | 49DC4A691B14F1BC00B4E78E /* Release */, 772 | ); 773 | defaultConfigurationIsVisible = 0; 774 | defaultConfigurationName = Release; 775 | }; 776 | 49DC4A6A1B14F1BC00B4E78E /* Build configuration list for PBXNativeTarget "ImagePickerSheetControllerTests" */ = { 777 | isa = XCConfigurationList; 778 | buildConfigurations = ( 779 | 49DC4A6B1B14F1BC00B4E78E /* Debug */, 780 | 49DC4A6C1B14F1BC00B4E78E /* Release */, 781 | ); 782 | defaultConfigurationIsVisible = 0; 783 | defaultConfigurationName = Release; 784 | }; 785 | 49DC4AA21B14F31500B4E78E /* Build configuration list for PBXNativeTarget "Example" */ = { 786 | isa = XCConfigurationList; 787 | buildConfigurations = ( 788 | 49DC4AA31B14F31500B4E78E /* Debug */, 789 | 49DC4AA41B14F31500B4E78E /* Release */, 790 | ); 791 | defaultConfigurationIsVisible = 0; 792 | defaultConfigurationName = Release; 793 | }; 794 | /* End XCConfigurationList section */ 795 | }; 796 | rootObject = 49DC4A481B14F1BC00B4E78E /* Project object */; 797 | } 798 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController.xcodeproj/project.xcworkspace/xcshareddata/ImagePickerSheetController.xccheckout: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDESourceControlProjectFavoriteDictionaryKey 6 | 7 | IDESourceControlProjectIdentifier 8 | 9BC4CC99-E09A-4C7E-8C5C-CDAA4C28FBEB 9 | IDESourceControlProjectName 10 | ImagePickerSheetController 11 | IDESourceControlProjectOriginsDictionary 12 | 13 | 81B80757B952CD09B96E04F606EF5F58B23F2659 14 | https://github.com/larcus94/ImagePickerSheetController.git 15 | 16 | IDESourceControlProjectPath 17 | ImagePickerSheetController/ImagePickerSheetController.xcodeproj 18 | IDESourceControlProjectRelativeInstallPathDictionary 19 | 20 | 81B80757B952CD09B96E04F606EF5F58B23F2659 21 | ../../.. 22 | 23 | IDESourceControlProjectURL 24 | https://github.com/larcus94/ImagePickerSheetController.git 25 | IDESourceControlProjectVersion 26 | 111 27 | IDESourceControlProjectWCCIdentifier 28 | 81B80757B952CD09B96E04F606EF5F58B23F2659 29 | IDESourceControlProjectWCConfigurations 30 | 31 | 32 | IDESourceControlRepositoryExtensionIdentifierKey 33 | public.vcs.git 34 | IDESourceControlWCCIdentifierKey 35 | 81B80757B952CD09B96E04F606EF5F58B23F2659 36 | IDESourceControlWCCName 37 | ImagePickerSheetController 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController.xcodeproj/xcshareddata/xcschemes/ImagePickerSheetController.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 43 | 49 | 50 | 51 | 52 | 53 | 59 | 60 | 62 | 68 | 69 | 70 | 71 | 72 | 78 | 79 | 80 | 81 | 82 | 83 | 93 | 94 | 100 | 101 | 102 | 103 | 104 | 105 | 111 | 112 | 118 | 119 | 120 | 121 | 123 | 124 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController.xcodeproj/xcuserdata/Laurin.xcuserdatad/xcschemes/Example.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 55 | 65 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 84 | 86 | 92 | 93 | 94 | 95 | 97 | 98 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController.xcodeproj/xcuserdata/Laurin.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Example.xcscheme 8 | 9 | orderHint 10 | 1 11 | 12 | ImagePickerSheetController.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 49DC4A501B14F1BC00B4E78E 21 | 22 | primary 23 | 24 | 25 | 49DC4A5B1B14F1BC00B4E78E 26 | 27 | primary 28 | 29 | 30 | 49DC4A851B14F31500B4E78E 31 | 32 | primary 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController.xcodeproj/xcuserdata/patrickbalestra.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Example.xcscheme 8 | 9 | orderHint 10 | 1 11 | 12 | ImagePickerSheetController.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 0 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 49DC4A501B14F1BC00B4E78E 21 | 22 | primary 23 | 24 | 25 | 49DC4A5B1B14F1BC00B4E78E 26 | 27 | primary 28 | 29 | 30 | 49DC4A851B14F31500B4E78E 31 | 32 | primary 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/AnimationController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnimationController.swift 3 | // ImagePickerSheet 4 | // 5 | // Created by Laurin Brandner on 25/05/15. 6 | // Copyright (c) 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class AnimationController: NSObject { 12 | 13 | let imagePickerSheetController: ImagePickerSheetController 14 | let presenting: Bool 15 | 16 | // MARK: - Initialization 17 | 18 | init(imagePickerSheetController: ImagePickerSheetController, presenting: Bool) { 19 | self.imagePickerSheetController = imagePickerSheetController 20 | self.presenting = presenting 21 | } 22 | 23 | // MARK: - Animation 24 | 25 | fileprivate func animatePresentation(_ context: UIViewControllerContextTransitioning) { 26 | let containerView = context.containerView 27 | containerView.addSubview(imagePickerSheetController.view) 28 | 29 | let sheetOriginY = imagePickerSheetController.sheetCollectionView.frame.origin.y 30 | imagePickerSheetController.sheetCollectionView.frame.origin.y = containerView.bounds.maxY 31 | imagePickerSheetController.backgroundView.alpha = 0 32 | 33 | UIView.animate(withDuration: transitionDuration(using: context), delay: 0, options: .curveEaseOut, animations: { () -> Void in 34 | self.imagePickerSheetController.sheetCollectionView.frame.origin.y = sheetOriginY 35 | self.imagePickerSheetController.backgroundView.alpha = 1 36 | }, completion: { _ in 37 | context.completeTransition(true) 38 | }) 39 | } 40 | 41 | fileprivate func animateDismissal(_ context: UIViewControllerContextTransitioning) { 42 | let containerView = context.containerView 43 | 44 | UIView.animate(withDuration: transitionDuration(using: context), delay: 0, options: .curveEaseIn, animations: { () -> Void in 45 | self.imagePickerSheetController.sheetCollectionView.frame.origin.y = containerView.bounds.maxY 46 | self.imagePickerSheetController.backgroundView.alpha = 0 47 | }, completion: { _ in 48 | self.imagePickerSheetController.view.removeFromSuperview() 49 | context.completeTransition(true) 50 | }) 51 | } 52 | 53 | } 54 | 55 | // MARK: - UIViewControllerAnimatedTransitioning 56 | extension AnimationController: UIViewControllerAnimatedTransitioning { 57 | 58 | func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { 59 | return 0.25 60 | } 61 | 62 | func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { 63 | if presenting { 64 | animatePresentation(transitionContext) 65 | } 66 | else { 67 | animateDismissal(transitionContext) 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/ImagePickerAction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerAction.swift 3 | // ImagePickerSheet 4 | // 5 | // Created by Laurin Brandner on 24/05/15. 6 | // Copyright (c) 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public enum ImagePickerActionStyle { 12 | case `default` 13 | case cancel 14 | } 15 | 16 | open class ImagePickerAction { 17 | 18 | public typealias Title = (Int) -> String 19 | public typealias Handler = (ImagePickerAction) -> () 20 | public typealias SecondaryHandler = (ImagePickerAction, Int) -> () 21 | 22 | /// The title of the action's button. 23 | public let title: String 24 | 25 | /// The title of the action's button when more than one image is selected. 26 | public let secondaryTitle: Title 27 | 28 | /// The style of the action. This is used to call a cancel handler when dismissing the controller by tapping the background. 29 | public let style: ImagePickerActionStyle 30 | 31 | fileprivate let handler: Handler? 32 | fileprivate let secondaryHandler: SecondaryHandler? 33 | 34 | /// Initializes a new cancel ImagePickerAction 35 | public init(cancelTitle: String) { 36 | self.title = cancelTitle 37 | self.secondaryTitle = { _ in cancelTitle } 38 | self.style = .cancel 39 | self.handler = nil 40 | self.secondaryHandler = nil 41 | } 42 | 43 | /// Initializes a new ImagePickerAction. The secondary title and handler are used when at least 1 image has been selected. 44 | /// Secondary title defaults to title if not specified. 45 | /// Secondary handler defaults to handler if not specified. 46 | public convenience init(title: String, secondaryTitle rawSecondaryTitle: String? = nil, style: ImagePickerActionStyle = .default, handler: @escaping Handler, secondaryHandler: SecondaryHandler? = nil) { 47 | let secondaryTitle: Title? = rawSecondaryTitle.map { string in 48 | return { _ in string } 49 | } 50 | self.init(title: title, secondaryTitle: secondaryTitle, style: style, handler: handler, secondaryHandler: secondaryHandler) 51 | } 52 | 53 | /// Initializes a new ImagePickerAction. The secondary title and handler are used when at least 1 image has been selected. 54 | /// Secondary title defaults to title if not specified. Use the closure to format a title according to the selection. 55 | /// Secondary handler defaults to handler if not specified 56 | public init(title: String, secondaryTitle: Title?, style: ImagePickerActionStyle = .default, handler: @escaping Handler, secondaryHandler secondaryHandlerOrNil: SecondaryHandler? = nil) { 57 | var secondaryHandler = secondaryHandlerOrNil 58 | if secondaryHandler == nil { 59 | secondaryHandler = { action, _ in 60 | handler(action) 61 | } 62 | } 63 | 64 | self.title = title 65 | self.secondaryTitle = secondaryTitle ?? { _ in title } 66 | self.style = style 67 | self.handler = handler 68 | self.secondaryHandler = secondaryHandler 69 | } 70 | 71 | func handle(_ numberOfImages: Int = 0) { 72 | if numberOfImages > 0 { 73 | secondaryHandler?(self, numberOfImages) 74 | } 75 | else { 76 | handler?(self) 77 | } 78 | } 79 | 80 | } 81 | 82 | func ?? (left: ImagePickerAction.Title?, right: @escaping ImagePickerAction.Title) -> ImagePickerAction.Title { 83 | if let left = left { 84 | return left 85 | } 86 | 87 | return right 88 | } 89 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/ImagePickerSheetController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerSheetController.h 3 | // ImagePickerSheetController 4 | // 5 | // Created by Laurin Brandner on 26/05/15. 6 | // Copyright (c) 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | //! Project version number for ImagePickerSheetController. 12 | FOUNDATION_EXPORT double ImagePickerSheetControllerVersionNumber; 13 | 14 | //! Project version string for ImagePickerSheetController. 15 | FOUNDATION_EXPORT const unsigned char ImagePickerSheetControllerVersionString[]; 16 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/ImagePickerSheetController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerController.swift 3 | // ImagePickerSheet 4 | // 5 | // Created by Laurin Brandner on 24/05/15. 6 | // Copyright (c) 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Photos 11 | 12 | let previewInset: CGFloat = 5 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 ImagePickerSheetControllerDelegate { 22 | 23 | @objc optional func controllerWillEnlargePreview(_ controller: ImagePickerSheetController) 24 | @objc optional func controllerDidEnlargePreview(_ controller: ImagePickerSheetController) 25 | 26 | @objc optional func controller(_ controller: ImagePickerSheetController, willSelectAsset asset: PHAsset) 27 | @objc optional func controller(_ controller: ImagePickerSheetController, didSelectAsset asset: PHAsset) 28 | 29 | @objc optional func controller(_ controller: ImagePickerSheetController, willDeselectAsset asset: PHAsset) 30 | @objc optional func controller(_ controller: ImagePickerSheetController, didDeselectAsset asset: PHAsset) 31 | 32 | } 33 | 34 | @available(iOS 9.0, *) 35 | public final class ImagePickerSheetController: UIViewController { 36 | 37 | fileprivate lazy var sheetController: SheetController = { 38 | let controller = SheetController(previewCollectionView: self.previewCollectionView) 39 | controller.actionHandlingCallback = { [weak self] in 40 | // Possible retain cycle when action handlers hold a reference to the IPSC 41 | // Remove all actions to break it 42 | self?.dismiss(animated: true, completion: { 43 | controller.removeAllActions() 44 | }) 45 | } 46 | 47 | return controller 48 | }() 49 | 50 | // self?.dismiss(animated: true, completion: { _ in 51 | // // Possible retain cycle when action handlers hold a reference to the IPSC 52 | // // Remove all actions to break it 53 | // controller.removeAllActions() 54 | // }) 55 | var sheetCollectionView: UICollectionView { 56 | return sheetController.sheetCollectionView 57 | } 58 | 59 | fileprivate(set) lazy var previewCollectionView: PreviewCollectionView = { 60 | let collectionView = PreviewCollectionView() 61 | collectionView.accessibilityIdentifier = "ImagePickerSheetPreview" 62 | collectionView.backgroundColor = .clear 63 | collectionView.allowsMultipleSelection = true 64 | collectionView.imagePreviewLayout.sectionInset = UIEdgeInsets(top: previewInset, left: previewInset, bottom: previewInset, right: previewInset) 65 | collectionView.imagePreviewLayout.showsSupplementaryViews = false 66 | collectionView.dataSource = self 67 | collectionView.delegate = self 68 | collectionView.showsHorizontalScrollIndicator = false 69 | collectionView.alwaysBounceHorizontal = true 70 | collectionView.register(PreviewCollectionViewCell.self, forCellWithReuseIdentifier: NSStringFromClass(PreviewCollectionViewCell.self)) 71 | collectionView.register(PreviewSupplementaryView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: NSStringFromClass(PreviewSupplementaryView.self)) 72 | 73 | return collectionView 74 | }() 75 | 76 | fileprivate var supplementaryViews = [Int: PreviewSupplementaryView]() 77 | 78 | lazy var backgroundView: UIView = { 79 | let view = UIView() 80 | view.accessibilityIdentifier = "ImagePickerSheetBackground" 81 | view.backgroundColor = UIColor(white: 0.0, alpha: 0.3961) 82 | view.addGestureRecognizer(UITapGestureRecognizer(target: self.sheetController, action: #selector(SheetController.handleCancelAction))) 83 | 84 | return view 85 | }() 86 | 87 | public var delegate: ImagePickerSheetControllerDelegate? 88 | 89 | /// All the actions. The first action is shown at the top. 90 | public var actions: [ImagePickerAction] { 91 | return sheetController.actions 92 | } 93 | 94 | /// Maximum selection of images. 95 | public var maximumSelection: Int? 96 | 97 | fileprivate var selectedAssetIndices = [Int]() { 98 | didSet { 99 | sheetController.numberOfSelectedAssets = selectedAssetIndices.count 100 | } 101 | } 102 | 103 | /// The selected image assets 104 | public var selectedAssets: [PHAsset] { 105 | return selectedAssetIndices.map { self.assets[$0] } 106 | } 107 | 108 | /// The media type of the displayed assets 109 | public let mediaType: ImagePickerMediaType 110 | 111 | fileprivate var assets = [PHAsset]() 112 | 113 | fileprivate lazy var requestOptions: PHImageRequestOptions = { 114 | let options = PHImageRequestOptions() 115 | options.deliveryMode = .highQualityFormat 116 | options.resizeMode = .fast 117 | 118 | return options 119 | }() 120 | 121 | fileprivate let imageManager = PHCachingImageManager() 122 | 123 | /// Whether the image preview has been elarged. This is the case when at least once 124 | /// image has been selected. 125 | public fileprivate(set) var enlargedPreviews = false 126 | 127 | fileprivate let minimumPreviewHeight: CGFloat = 129 128 | fileprivate var maximumPreviewHeight: CGFloat = 129 129 | 130 | fileprivate var previewCheckmarkInset: CGFloat { 131 | return 12.5 132 | } 133 | 134 | // MARK: - Initialization 135 | 136 | public init(mediaType: ImagePickerMediaType) { 137 | self.mediaType = mediaType 138 | super.init(nibName: nil, bundle: nil) 139 | initialize() 140 | } 141 | 142 | public required init?(coder aDecoder: NSCoder) { 143 | self.mediaType = .imageAndVideo 144 | super.init(coder: aDecoder) 145 | initialize() 146 | } 147 | 148 | fileprivate func initialize() { 149 | modalPresentationStyle = .custom 150 | transitioningDelegate = self 151 | 152 | NotificationCenter.default.addObserver(sheetController, selector: #selector(sheetController.handleCancelAction), name: UIApplication.didEnterBackgroundNotification, object: nil) 153 | } 154 | 155 | @objc deinit { 156 | NotificationCenter.default.removeObserver(sheetController, name: UIApplication.didEnterBackgroundNotification, object: nil) 157 | } 158 | 159 | // MARK: - View Lifecycle 160 | 161 | override public func loadView() { 162 | super.loadView() 163 | 164 | view.addSubview(backgroundView) 165 | view.addSubview(sheetCollectionView) 166 | } 167 | 168 | override public func viewWillAppear(_ animated: Bool) { 169 | super.viewWillAppear(animated) 170 | 171 | preferredContentSize = CGSize(width: 400, height: view.frame.height) 172 | 173 | if PHPhotoLibrary.authorizationStatus() == .authorized { 174 | prepareAssets() 175 | } 176 | } 177 | 178 | override public func viewDidAppear(_ animated: Bool) { 179 | super.viewDidAppear(animated) 180 | 181 | if PHPhotoLibrary.authorizationStatus() == .notDetermined { 182 | PHPhotoLibrary.requestAuthorization() { status in 183 | if status == .authorized { 184 | DispatchQueue.main.async { 185 | self.prepareAssets() 186 | self.previewCollectionView.reloadData() 187 | self.sheetCollectionView.reloadData() 188 | self.view.setNeedsLayout() 189 | 190 | // Explicitely disable animations so it wouldn't animate either 191 | // if it was in a popover 192 | CATransaction.begin() 193 | CATransaction.setDisableActions(true) 194 | self.view.layoutIfNeeded() 195 | CATransaction.commit() 196 | } 197 | } 198 | } 199 | } 200 | } 201 | 202 | // MARK: - Actions 203 | 204 | /// Adds an new action. 205 | /// If the passed action is of type Cancel, any pre-existing Cancel actions will be removed. 206 | /// Always arranges the actions so that the Cancel action appears at the bottom. 207 | public func addAction(_ action: ImagePickerAction) { 208 | sheetController.addAction(action) 209 | view.setNeedsLayout() 210 | } 211 | 212 | // MARK: - Images 213 | 214 | fileprivate func sizeForAsset(_ asset: PHAsset, scale: CGFloat = 1) -> CGSize { 215 | let proportion = CGFloat(asset.pixelWidth)/CGFloat(asset.pixelHeight) 216 | 217 | let imageHeight = maximumPreviewHeight - 2 * previewInset 218 | let imageWidth = floor(proportion * imageHeight) 219 | 220 | return CGSize(width: imageWidth * scale, height: imageHeight * scale) 221 | } 222 | 223 | fileprivate func prepareAssets() { 224 | fetchAssets() 225 | reloadMaximumPreviewHeight() 226 | reloadCurrentPreviewHeight(invalidateLayout: false) 227 | 228 | // Filter out the assets that are too thin. This can't be done before because 229 | // we don't know how tall the images should be 230 | let minImageWidth = 2 * previewCheckmarkInset + (PreviewSupplementaryView.checkmarkImage?.size.width ?? 0) 231 | assets = assets.filter { asset in 232 | let size = sizeForAsset(asset) 233 | return size.width >= minImageWidth 234 | } 235 | } 236 | 237 | fileprivate func fetchAssets() { 238 | let options = PHFetchOptions() 239 | options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] 240 | 241 | switch mediaType { 242 | case .image: 243 | options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue) 244 | case .video: 245 | options.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.video.rawValue) 246 | case .imageAndVideo: 247 | options.predicate = NSPredicate(format: "mediaType = %d OR mediaType = %d", PHAssetMediaType.image.rawValue, PHAssetMediaType.video.rawValue) 248 | } 249 | 250 | let fetchLimit = 50 251 | options.fetchLimit = fetchLimit 252 | 253 | let result = PHAsset.fetchAssets(with: options) 254 | let requestOptions = PHImageRequestOptions() 255 | requestOptions.isSynchronous = true 256 | requestOptions.deliveryMode = .fastFormat 257 | 258 | result.enumerateObjects(options: [], using: { asset, index, stop in 259 | defer { 260 | if self.assets.count > fetchLimit { 261 | stop.initialize(to: true) 262 | } 263 | } 264 | 265 | self.imageManager.requestImageData(for: asset, options: requestOptions) { data, _, _, info in 266 | if data != nil { 267 | self.assets.append(asset) 268 | } 269 | } 270 | }) 271 | } 272 | 273 | fileprivate func requestImageForAsset(_ asset: PHAsset, completion: @escaping (_ image: UIImage?) -> ()) { 274 | let targetSize = sizeForAsset(asset, scale: UIScreen.main.scale) 275 | requestOptions.isSynchronous = true 276 | 277 | // Workaround because PHImageManager.requestImageForAsset doesn't work for burst images 278 | if asset.representsBurst { 279 | imageManager.requestImageData(for: asset, options: requestOptions) { data, _, _, _ in 280 | let image = data.flatMap { UIImage(data: $0) } 281 | completion(image) 282 | } 283 | } 284 | else { 285 | imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: requestOptions) { image, _ in 286 | completion(image) 287 | } 288 | } 289 | } 290 | 291 | fileprivate func prefetchImagesForAsset(_ asset: PHAsset) { 292 | let targetSize = sizeForAsset(asset, scale: UIScreen.main.scale) 293 | imageManager.startCachingImages(for: [asset], targetSize: targetSize, contentMode: .aspectFill, options: requestOptions) 294 | } 295 | 296 | // MARK: - Layout 297 | 298 | override public func viewDidLayoutSubviews() { 299 | super.viewDidLayoutSubviews() 300 | 301 | if popoverPresentationController == nil { 302 | // Offset necessary for expanded status bar 303 | // Bug in UIKit which doesn't reset the view's frame correctly 304 | 305 | let offset = UIApplication.shared.statusBarFrame.height 306 | var backgroundViewFrame = UIScreen.main.bounds 307 | backgroundViewFrame.origin.y = -offset 308 | backgroundViewFrame.size.height += offset 309 | backgroundView.frame = backgroundViewFrame 310 | } 311 | else { 312 | backgroundView.frame = view.bounds 313 | } 314 | 315 | reloadMaximumPreviewHeight() 316 | reloadCurrentPreviewHeight(invalidateLayout: true) 317 | 318 | let sheetHeight = sheetController.preferredSheetHeight 319 | let sheetSize = CGSize(width: view.bounds.width, height: sheetHeight) 320 | 321 | // This particular order is necessary so that the sheet is layed out 322 | // correctly with and without an enclosing popover 323 | preferredContentSize = sheetSize 324 | sheetCollectionView.frame = CGRect(origin: CGPoint(x: view.bounds.minX, y: view.bounds.maxY - view.frame.origin.y - sheetHeight), size: sheetSize) 325 | } 326 | 327 | fileprivate func reloadCurrentPreviewHeight(invalidateLayout invalidate: Bool) { 328 | if assets.count <= 0 { 329 | sheetController.setPreviewHeight(0, invalidateLayout: invalidate) 330 | } 331 | else if assets.count > 0 && enlargedPreviews { 332 | sheetController.setPreviewHeight(maximumPreviewHeight, invalidateLayout: invalidate) 333 | } 334 | else { 335 | sheetController.setPreviewHeight(minimumPreviewHeight, invalidateLayout: invalidate) 336 | } 337 | } 338 | 339 | fileprivate func reloadMaximumPreviewHeight() { 340 | let maxHeight: CGFloat = 400 341 | let maxImageWidth = view.bounds.width - 2 * sheetInset - 2 * previewInset 342 | 343 | let assetRatios = assets.map { (asset: PHAsset) -> CGSize in 344 | CGSize(width: max(asset.pixelHeight, asset.pixelWidth), height: min(asset.pixelHeight, asset.pixelWidth)) 345 | }.map { (size: CGSize) -> CGFloat in 346 | size.height / size.width 347 | } 348 | 349 | let assetHeights = assetRatios.map { (ratio: CGFloat) -> CGFloat in ratio * maxImageWidth } 350 | .filter { (height: CGFloat) -> Bool in height < maxImageWidth && height < maxHeight } // Make sure the preview isn't too high eg for squares 351 | .sorted(by: >) 352 | let assetHeight: CGFloat 353 | if let first = assetHeights.first { 354 | assetHeight = first 355 | } 356 | else { 357 | assetHeight = 0 358 | } 359 | 360 | // Just a sanity check, to make sure this doesn't exceed 400 points 361 | let scaledHeight: CGFloat = min(assetHeight, maxHeight) 362 | maximumPreviewHeight = scaledHeight + 2 * previewInset 363 | } 364 | 365 | // MARK: - 366 | 367 | func enlargePreviewsByCenteringToIndexPath(_ indexPath: IndexPath?, completion: (() -> ())?) { 368 | enlargedPreviews = true 369 | previewCollectionView.imagePreviewLayout.invalidationCenteredIndexPath = indexPath 370 | reloadCurrentPreviewHeight(invalidateLayout: false) 371 | 372 | view.setNeedsLayout() 373 | 374 | self.delegate?.controllerWillEnlargePreview?(self) 375 | 376 | UIView.animate(withDuration: 0.2, animations: { 377 | self.view.layoutIfNeeded() 378 | self.sheetCollectionView.collectionViewLayout.invalidateLayout() 379 | }, completion: { _ in 380 | self.delegate?.controllerDidEnlargePreview?(self) 381 | 382 | completion?() 383 | }) 384 | } 385 | 386 | } 387 | 388 | // MARK: - UICollectionViewDataSource 389 | 390 | extension ImagePickerSheetController: UICollectionViewDataSource { 391 | 392 | public func numberOfSections(in collectionView: UICollectionView) -> Int { 393 | return assets.count 394 | } 395 | 396 | public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 397 | return 1 398 | } 399 | 400 | public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 401 | let cell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(PreviewCollectionViewCell.self), for: indexPath) as! PreviewCollectionViewCell 402 | 403 | let asset = assets[indexPath.section] 404 | cell.videoIndicatorView.isHidden = asset.mediaType != .video 405 | 406 | requestImageForAsset(asset) { image in 407 | cell.imageView.image = image 408 | } 409 | 410 | cell.isSelected = selectedAssetIndices.contains(indexPath.section) 411 | 412 | return cell 413 | } 414 | 415 | public func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: 416 | IndexPath) -> UICollectionReusableView { 417 | let view = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: NSStringFromClass(PreviewSupplementaryView.self), for: indexPath) as! PreviewSupplementaryView 418 | view.isUserInteractionEnabled = false 419 | view.buttonInset = UIEdgeInsets(top: 0.0, left: previewCheckmarkInset, bottom: previewCheckmarkInset, right: 0.0) 420 | view.selected = selectedAssetIndices.contains(indexPath.section) 421 | 422 | supplementaryViews[indexPath.section] = view 423 | 424 | return view 425 | } 426 | 427 | } 428 | 429 | // MARK: - UICollectionViewDelegate 430 | 431 | extension ImagePickerSheetController: UICollectionViewDelegate { 432 | 433 | public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 434 | if let maximumSelection = maximumSelection { 435 | if selectedAssetIndices.count >= maximumSelection, 436 | let previousItemIndex = selectedAssetIndices.first { 437 | let deselectedAsset = assets[previousItemIndex] 438 | delegate?.controller?(self, willDeselectAsset: deselectedAsset) 439 | 440 | supplementaryViews[previousItemIndex]?.selected = false 441 | selectedAssetIndices.remove(at: 0) 442 | 443 | delegate?.controller?(self, didDeselectAsset: deselectedAsset) 444 | } 445 | } 446 | 447 | let selectedAsset = assets[indexPath.section] 448 | delegate?.controller?(self, willSelectAsset: selectedAsset) 449 | 450 | // Just to make sure the image is only selected once 451 | selectedAssetIndices = selectedAssetIndices.filter { $0 != indexPath.section } 452 | selectedAssetIndices.append(indexPath.section) 453 | 454 | if !enlargedPreviews { 455 | enlargePreviewsByCenteringToIndexPath(indexPath) { 456 | self.sheetController.reloadActionItems() 457 | self.previewCollectionView.imagePreviewLayout.showsSupplementaryViews = true 458 | } 459 | } 460 | else { 461 | // scrollToItemAtIndexPath doesn't work reliably 462 | if let cell = collectionView.cellForItem(at: indexPath) { 463 | var contentOffset = CGPoint(x: cell.frame.midX - collectionView.frame.width / 2.0, y: 0.0) 464 | contentOffset.x = max(contentOffset.x, -collectionView.contentInset.left) 465 | contentOffset.x = min(contentOffset.x, collectionView.contentSize.width - collectionView.frame.width + collectionView.contentInset.right) 466 | 467 | collectionView.setContentOffset(contentOffset, animated: true) 468 | } 469 | 470 | sheetController.reloadActionItems() 471 | } 472 | 473 | supplementaryViews[indexPath.section]?.selected = true 474 | 475 | delegate?.controller?(self, didSelectAsset: selectedAsset) 476 | } 477 | 478 | public func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { 479 | if let index = selectedAssetIndices.index(of: indexPath.section) { 480 | let deselectedAsset = selectedAssets[index] 481 | delegate?.controller?(self, willDeselectAsset: deselectedAsset) 482 | 483 | selectedAssetIndices.remove(at: index) 484 | sheetController.reloadActionItems() 485 | 486 | delegate?.controller?(self, didDeselectAsset: deselectedAsset) 487 | } 488 | 489 | supplementaryViews[indexPath.section]?.selected = false 490 | } 491 | 492 | } 493 | 494 | // MARK: - UICollectionViewDelegateFlowLayout 495 | 496 | extension ImagePickerSheetController: UICollectionViewDelegateFlowLayout { 497 | 498 | public func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 499 | let asset = assets[indexPath.section] 500 | let size = sizeForAsset(asset) 501 | 502 | // Scale down to the current preview height, sizeForAsset returns the original size 503 | let currentImagePreviewHeight = sheetController.previewHeight - 2 * previewInset 504 | let scale = currentImagePreviewHeight / size.height 505 | 506 | return CGSize(width: size.width * scale, height: currentImagePreviewHeight) 507 | } 508 | 509 | public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize { 510 | let checkmarkWidth = PreviewSupplementaryView.checkmarkImage?.size.width ?? 0 511 | return CGSize(width: checkmarkWidth + 2 * previewCheckmarkInset, height: sheetController.previewHeight - 2 * previewInset) 512 | } 513 | 514 | } 515 | 516 | // MARK: - UIViewControllerTransitioningDelegate 517 | 518 | extension ImagePickerSheetController: UIViewControllerTransitioningDelegate { 519 | 520 | public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { 521 | return AnimationController(imagePickerSheetController: self, presenting: true) 522 | } 523 | 524 | public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { 525 | return AnimationController(imagePickerSheetController: self, presenting: false) 526 | } 527 | 528 | } 529 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewCollectionViewCell-video.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "PreviewCollectionViewCell-video.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "PreviewCollectionViewCell-video@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "PreviewCollectionViewCell-video@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewCollectionViewCell-video.imageset/PreviewCollectionViewCell-video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerSheetController/c57fa10ad847e29cabfa90a60baffca3c67474b8/ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewCollectionViewCell-video.imageset/PreviewCollectionViewCell-video.png -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewCollectionViewCell-video.imageset/PreviewCollectionViewCell-video@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerSheetController/c57fa10ad847e29cabfa90a60baffca3c67474b8/ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewCollectionViewCell-video.imageset/PreviewCollectionViewCell-video@2x.png -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewCollectionViewCell-video.imageset/PreviewCollectionViewCell-video@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerSheetController/c57fa10ad847e29cabfa90a60baffca3c67474b8/ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewCollectionViewCell-video.imageset/PreviewCollectionViewCell-video@3x.png -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewSupplementaryView-Checkmark-Selected.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "PreviewSupplementaryView-Checkmark-Selected.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "PreviewSupplementaryView-Checkmark-Selected@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "PreviewSupplementaryView-Checkmark-Selected@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewSupplementaryView-Checkmark-Selected.imageset/PreviewSupplementaryView-Checkmark-Selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerSheetController/c57fa10ad847e29cabfa90a60baffca3c67474b8/ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewSupplementaryView-Checkmark-Selected.imageset/PreviewSupplementaryView-Checkmark-Selected.png -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewSupplementaryView-Checkmark-Selected.imageset/PreviewSupplementaryView-Checkmark-Selected@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerSheetController/c57fa10ad847e29cabfa90a60baffca3c67474b8/ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewSupplementaryView-Checkmark-Selected.imageset/PreviewSupplementaryView-Checkmark-Selected@2x.png -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewSupplementaryView-Checkmark-Selected.imageset/PreviewSupplementaryView-Checkmark-Selected@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerSheetController/c57fa10ad847e29cabfa90a60baffca3c67474b8/ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewSupplementaryView-Checkmark-Selected.imageset/PreviewSupplementaryView-Checkmark-Selected@3x.png -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewSupplementaryView-Checkmark.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x", 6 | "filename" : "PreviewSupplementaryView-Checkmark.png" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x", 11 | "filename" : "PreviewSupplementaryView-Checkmark@2x.png" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x", 16 | "filename" : "PreviewSupplementaryView-Checkmark@3x.png" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewSupplementaryView-Checkmark.imageset/PreviewSupplementaryView-Checkmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerSheetController/c57fa10ad847e29cabfa90a60baffca3c67474b8/ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewSupplementaryView-Checkmark.imageset/PreviewSupplementaryView-Checkmark.png -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewSupplementaryView-Checkmark.imageset/PreviewSupplementaryView-Checkmark@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerSheetController/c57fa10ad847e29cabfa90a60baffca3c67474b8/ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewSupplementaryView-Checkmark.imageset/PreviewSupplementaryView-Checkmark@2x.png -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewSupplementaryView-Checkmark.imageset/PreviewSupplementaryView-Checkmark@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerSheetController/c57fa10ad847e29cabfa90a60baffca3c67474b8/ImagePickerSheetController/ImagePickerSheetController/Images.xcassets/PreviewSupplementaryView-Checkmark.imageset/PreviewSupplementaryView-Checkmark@3x.png -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/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 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(CURRENT_PROJECT_VERSION) 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Sheet/Preview/PreviewCollectionView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewCollectionView.swift 3 | // ImagePickerSheet 4 | // 5 | // Created by Laurin Brandner on 07/09/14. 6 | // Copyright (c) 2014 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PreviewCollectionView: UICollectionView { 12 | 13 | var bouncing: Bool { 14 | if contentOffset.x < -contentInset.left { return true } 15 | if contentOffset.x + frame.width > contentSize.width + contentInset.right { return true } 16 | return false 17 | } 18 | 19 | var imagePreviewLayout: PreviewCollectionViewLayout { 20 | return collectionViewLayout as! PreviewCollectionViewLayout 21 | } 22 | 23 | // MARK: - Initialization 24 | 25 | init() { 26 | super.init(frame: .zero, collectionViewLayout: PreviewCollectionViewLayout()) 27 | 28 | initialize() 29 | } 30 | 31 | required init?(coder aDecoder: NSCoder) { 32 | super.init(coder: aDecoder) 33 | initialize() 34 | } 35 | 36 | fileprivate func initialize() { 37 | panGestureRecognizer.addTarget(self, action: #selector(PreviewCollectionView.handlePanGesture(gestureRecognizer:))) 38 | } 39 | 40 | // MARK: - Panning 41 | 42 | @objc private func handlePanGesture(gestureRecognizer: UIPanGestureRecognizer) { 43 | if gestureRecognizer.state == .ended { 44 | let translation = gestureRecognizer.translation(in: self) 45 | if translation == CGPoint() { 46 | if !bouncing { 47 | let possibleIndexPath = indexPathForItem(at: gestureRecognizer.location(in: self)) 48 | if let indexPath = possibleIndexPath { 49 | selectItem(at: indexPath, animated: false, scrollPosition: []) 50 | delegate?.collectionView?(self, didSelectItemAt: indexPath) 51 | } 52 | } 53 | } 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Sheet/Preview/PreviewCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewCollectionViewCell.swift 3 | // ImagePickerSheet 4 | // 5 | // Created by Laurin Brandner on 06/09/14. 6 | // Copyright (c) 2014 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PreviewCollectionViewCell: 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 | let videoIndicatorView: UIImageView = { 22 | let imageView = UIImageView(image: videoImage) 23 | imageView.isHidden = true 24 | 25 | return imageView 26 | }() 27 | 28 | fileprivate class var videoImage: UIImage? { 29 | let bundle = Bundle(for: ImagePickerSheetController.self) 30 | let image = UIImage(named: "PreviewCollectionViewCell-video", in: bundle, compatibleWith: nil) 31 | 32 | return image 33 | } 34 | 35 | // MARK: - Initialization 36 | 37 | override init(frame: CGRect) { 38 | super.init(frame: frame) 39 | 40 | initialize() 41 | } 42 | 43 | required init?(coder aDecoder: NSCoder) { 44 | super.init(coder: aDecoder) 45 | 46 | initialize() 47 | } 48 | 49 | fileprivate func initialize() { 50 | addSubview(imageView) 51 | addSubview(videoIndicatorView) 52 | } 53 | 54 | // MARK: - Other Methods 55 | 56 | override func prepareForReuse() { 57 | super.prepareForReuse() 58 | 59 | imageView.image = nil 60 | videoIndicatorView.isHidden = true 61 | } 62 | 63 | // MARK: - Layout 64 | 65 | override func layoutSubviews() { 66 | super.layoutSubviews() 67 | 68 | imageView.frame = bounds 69 | 70 | let videoIndicatViewSize = videoIndicatorView.image?.size ?? CGSize() 71 | let inset: CGFloat = 4 72 | let videoIndicatorViewOrigin = CGPoint(x: bounds.minX + inset, y: bounds.maxY - inset - videoIndicatViewSize.height) 73 | videoIndicatorView.frame = CGRect(origin: videoIndicatorViewOrigin, size: videoIndicatViewSize) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Sheet/Preview/PreviewCollectionViewLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewCollectionViewLayout.swift 3 | // ImagePickerSheet 4 | // 5 | // Created by Laurin Brandner on 06/09/14. 6 | // Copyright (c) 2014 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PreviewCollectionViewLayout: UICollectionViewFlowLayout { 12 | 13 | var invalidationCenteredIndexPath: IndexPath? 14 | 15 | var showsSupplementaryViews: Bool = true { 16 | didSet { 17 | invalidateLayout() 18 | } 19 | } 20 | 21 | fileprivate var layoutAttributes = [UICollectionViewLayoutAttributes]() 22 | fileprivate var contentSize = CGSize.zero 23 | 24 | // MARK: - Initialization 25 | 26 | override init() { 27 | super.init() 28 | 29 | initialize() 30 | } 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | super.init(coder: aDecoder) 34 | 35 | initialize() 36 | } 37 | 38 | fileprivate func initialize() { 39 | scrollDirection = .horizontal 40 | } 41 | 42 | // MARK: - Layout 43 | 44 | override func prepare() { 45 | super.prepare() 46 | 47 | layoutAttributes.removeAll(keepingCapacity: false) 48 | contentSize = CGSize.zero 49 | 50 | if let collectionView = collectionView, 51 | let delegate = collectionView.delegate as? UICollectionViewDelegateFlowLayout { 52 | var origin = CGPoint(x: sectionInset.left, y: sectionInset.top) 53 | let numberOfSections = collectionView.numberOfSections 54 | 55 | for s in 0 ..< numberOfSections { 56 | guard collectionView.numberOfItems(inSection: s) > 0 57 | else { continue } 58 | 59 | let indexPath = IndexPath(item: 0, section: s) 60 | let size = delegate.collectionView?(collectionView, layout: self, sizeForItemAt: indexPath) ?? CGSize.zero 61 | 62 | let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) 63 | attributes.frame = CGRect(origin: origin, size: size) 64 | attributes.zIndex = 0 65 | 66 | layoutAttributes.append(attributes) 67 | 68 | origin.x = attributes.frame.maxX + sectionInset.right 69 | } 70 | 71 | contentSize = CGSize(width: origin.x, height: collectionView.frame.height) 72 | } 73 | } 74 | 75 | override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 76 | return true 77 | } 78 | 79 | override var collectionViewContentSize : CGSize { 80 | return contentSize 81 | } 82 | 83 | override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint { 84 | var contentOffset = proposedContentOffset 85 | if let indexPath = invalidationCenteredIndexPath { 86 | if let collectionView = collectionView { 87 | let frame = layoutAttributes[indexPath.section].frame 88 | contentOffset.x = frame.midX - collectionView.frame.width / 2.0 89 | 90 | contentOffset.x = max(contentOffset.x, -collectionView.contentInset.left) 91 | contentOffset.x = min(contentOffset.x, collectionViewContentSize.width - collectionView.frame.width + collectionView.contentInset.right) 92 | } 93 | invalidationCenteredIndexPath = nil 94 | } 95 | 96 | return super.targetContentOffset(forProposedContentOffset: contentOffset) 97 | } 98 | 99 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 100 | return layoutAttributes 101 | .filter { rect.intersects($0.frame) } 102 | .reduce([UICollectionViewLayoutAttributes]()) { memo, attributes in 103 | if let supplementaryAttributes = layoutAttributesForSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, at: attributes.indexPath) { 104 | return memo + [attributes, supplementaryAttributes] 105 | } 106 | return memo 107 | } 108 | } 109 | 110 | override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 111 | return layoutAttributes[indexPath.section] 112 | } 113 | 114 | override func layoutAttributesForSupplementaryView(ofKind elementKind: String, at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 115 | if let collectionView = collectionView, 116 | let delegate = collectionView.delegate as? UICollectionViewDelegateFlowLayout, 117 | let itemAttributes = layoutAttributesForItem(at: indexPath) { 118 | 119 | let inset = collectionView.contentInset 120 | let bounds = collectionView.bounds 121 | let contentOffset: CGPoint = { 122 | var contentOffset = collectionView.contentOffset 123 | contentOffset.x += inset.left 124 | contentOffset.y += inset.top 125 | 126 | return contentOffset 127 | }() 128 | let visibleSize: CGSize = { 129 | var size = bounds.size 130 | size.width -= (inset.left+inset.right) 131 | 132 | return size 133 | }() 134 | let visibleFrame = CGRect(origin: contentOffset, size: visibleSize) 135 | 136 | let size = delegate.collectionView?(collectionView, layout: self, referenceSizeForHeaderInSection: indexPath.section) ?? CGSize.zero 137 | let originX = max(itemAttributes.frame.minX, min(itemAttributes.frame.maxX - size.width, visibleFrame.maxX - size.width)) 138 | 139 | let attributes = UICollectionViewLayoutAttributes(forSupplementaryViewOfKind: elementKind, with: indexPath) 140 | attributes.zIndex = 1 141 | attributes.isHidden = !showsSupplementaryViews 142 | attributes.frame = CGRect(origin: CGPoint(x: originX, y: itemAttributes.frame.minY), size: size) 143 | 144 | return attributes 145 | } 146 | 147 | return nil 148 | } 149 | 150 | override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 151 | return layoutAttributesForItem(at: itemIndexPath) 152 | } 153 | 154 | override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 155 | return layoutAttributesForItem(at: itemIndexPath) 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Sheet/Preview/PreviewSupplementaryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PreviewSupplementaryView.swift 3 | // ImagePickerSheet 4 | // 5 | // Created by Laurin Brandner on 06/09/14. 6 | // Copyright (c) 2014 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class PreviewSupplementaryView: UICollectionReusableView { 12 | 13 | fileprivate let button: UIButton = { 14 | let button = UIButton() 15 | button.tintColor = .white 16 | button.isUserInteractionEnabled = false 17 | button.setImage(PreviewSupplementaryView.checkmarkImage, for: UIControl.State()) 18 | button.setImage(PreviewSupplementaryView.selectedCheckmarkImage, for: .selected) 19 | 20 | return button 21 | }() 22 | 23 | var buttonInset = UIEdgeInsets.zero 24 | 25 | var selected: Bool = false { 26 | didSet { 27 | button.isSelected = selected 28 | reloadButtonBackgroundColor() 29 | } 30 | } 31 | 32 | class var checkmarkImage: UIImage? { 33 | let bundle = Bundle(for: ImagePickerSheetController.self) 34 | let image = UIImage(named: "PreviewSupplementaryView-Checkmark", in: bundle, compatibleWith: nil) 35 | 36 | return image?.withRenderingMode(.alwaysTemplate) 37 | } 38 | 39 | class var selectedCheckmarkImage: UIImage? { 40 | let bundle = Bundle(for: ImagePickerSheetController.self) 41 | let image = UIImage(named: "PreviewSupplementaryView-Checkmark-Selected", in: bundle, compatibleWith: nil) 42 | 43 | return image?.withRenderingMode(.alwaysTemplate) 44 | } 45 | 46 | // MARK: - Initialization 47 | 48 | override init(frame: CGRect) { 49 | super.init(frame: frame) 50 | 51 | initialize() 52 | } 53 | 54 | required init?(coder aDecoder: NSCoder) { 55 | super.init(coder: aDecoder) 56 | 57 | initialize() 58 | } 59 | 60 | fileprivate func initialize() { 61 | addSubview(button) 62 | } 63 | 64 | // MARK: - Other Methods 65 | 66 | override func prepareForReuse() { 67 | super.prepareForReuse() 68 | 69 | selected = false 70 | } 71 | 72 | override func tintColorDidChange() { 73 | super.tintColorDidChange() 74 | 75 | reloadButtonBackgroundColor() 76 | } 77 | 78 | fileprivate func reloadButtonBackgroundColor() { 79 | button.backgroundColor = (selected) ? tintColor : nil 80 | } 81 | 82 | // MARK: - Layout 83 | 84 | override func layoutSubviews() { 85 | super.layoutSubviews() 86 | 87 | button.sizeToFit() 88 | button.frame.origin = CGPoint(x: buttonInset.left, y: bounds.height-button.frame.height-buttonInset.bottom) 89 | button.layer.cornerRadius = button.frame.height / 2.0 90 | } 91 | 92 | } 93 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Sheet/SheetActionCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SheetActionCollectionViewCell.swift 3 | // ImagePickerSheetController 4 | // 5 | // Created by Laurin Brandner on 26/08/15. 6 | // Copyright © 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | private var KVOContext = 0 12 | 13 | class SheetActionCollectionViewCell: SheetCollectionViewCell { 14 | 15 | lazy fileprivate(set) var textLabel: UILabel = { 16 | let label = UILabel() 17 | label.textColor = self.tintColor 18 | label.textAlignment = .center 19 | 20 | self.addSubview(label) 21 | 22 | return label 23 | }() 24 | 25 | // MARK: - Initialization 26 | 27 | override init(frame: CGRect) { 28 | super.init(frame: frame) 29 | initialize() 30 | } 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | super.init(coder: aDecoder) 34 | initialize() 35 | } 36 | 37 | fileprivate func initialize() { 38 | textLabel.addObserver(self, forKeyPath: "text", options: NSKeyValueObservingOptions(rawValue: 0), context: &KVOContext) 39 | } 40 | 41 | deinit { 42 | textLabel.removeObserver(self, forKeyPath: "text") 43 | } 44 | 45 | // MARK: - Accessibility 46 | 47 | override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { 48 | guard context == &KVOContext else { 49 | super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) 50 | return 51 | } 52 | 53 | accessibilityLabel = textLabel.text 54 | } 55 | 56 | // MARK: - 57 | 58 | override func tintColorDidChange() { 59 | super.tintColorDidChange() 60 | 61 | textLabel.textColor = tintColor 62 | } 63 | 64 | override func layoutSubviews() { 65 | super.layoutSubviews() 66 | textLabel.frame = bounds.inset(by: backgroundInsets) 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Sheet/SheetCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SheetCollectionViewCell.swift 3 | // ImagePickerSheetController 4 | // 5 | // Created by Laurin Brandner on 24/08/15. 6 | // Copyright © 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | enum RoundedCorner { 12 | case all(CGFloat) 13 | case top(CGFloat) 14 | case bottom(CGFloat) 15 | case none 16 | } 17 | 18 | class SheetCollectionViewCell: UICollectionViewCell { 19 | 20 | var backgroundInsets = UIEdgeInsets() { 21 | didSet { 22 | reloadMask() 23 | reloadSeparator() 24 | setNeedsLayout() 25 | } 26 | } 27 | 28 | var roundedCorners = RoundedCorner.none { 29 | didSet { 30 | reloadMask() 31 | } 32 | } 33 | 34 | var separatorVisible = false { 35 | didSet { 36 | reloadSeparator() 37 | } 38 | } 39 | 40 | var separatorColor = UIColor.black { 41 | didSet { 42 | separatorView?.backgroundColor = separatorColor 43 | } 44 | } 45 | 46 | var separatorHeight: CGFloat = 1 { 47 | didSet { 48 | setNeedsLayout() 49 | } 50 | } 51 | 52 | fileprivate var separatorView: UIView? 53 | 54 | override var isHighlighted: Bool { 55 | didSet { 56 | reloadBackgroundColor() 57 | } 58 | } 59 | 60 | var highlightedBackgroundColor: UIColor = .clear { 61 | didSet { 62 | reloadBackgroundColor() 63 | } 64 | } 65 | 66 | var normalBackgroundColor: UIColor = .clear { 67 | didSet { 68 | reloadBackgroundColor() 69 | } 70 | } 71 | 72 | fileprivate var needsMasking: Bool { 73 | guard backgroundInsets == UIEdgeInsets() else { 74 | return true 75 | } 76 | 77 | switch roundedCorners { 78 | case .none: 79 | return false 80 | default: 81 | return true 82 | } 83 | } 84 | 85 | // MARK: - Initialization 86 | 87 | override init(frame: CGRect) { 88 | super.init(frame: frame) 89 | initialize() 90 | } 91 | 92 | required init?(coder aDecoder: NSCoder) { 93 | super.init(coder: aDecoder) 94 | initialize() 95 | } 96 | 97 | fileprivate func initialize() { 98 | layoutMargins = UIEdgeInsets() 99 | } 100 | 101 | // MARK: - Layout 102 | 103 | override func layoutSubviews() { 104 | super.layoutSubviews() 105 | 106 | reloadMask() 107 | 108 | separatorView?.frame = CGRect(x: bounds.minY, y: bounds.maxY - separatorHeight, width: bounds.width, height: separatorHeight) 109 | } 110 | 111 | // MARK: - Mask 112 | 113 | fileprivate func reloadMask() { 114 | if needsMasking && layer.mask == nil { 115 | let maskLayer = CAShapeLayer() 116 | maskLayer.frame = bounds 117 | maskLayer.lineWidth = 0 118 | maskLayer.fillColor = UIColor.black.cgColor 119 | 120 | layer.mask = maskLayer 121 | } 122 | 123 | let layerMask = layer.mask as? CAShapeLayer 124 | layerMask?.frame = bounds 125 | layerMask?.path = maskPathWithRect(bounds.inset(by: backgroundInsets), roundedCorner: roundedCorners) 126 | } 127 | 128 | fileprivate func maskPathWithRect(_ rect: CGRect, roundedCorner: RoundedCorner) -> CGPath { 129 | let radii: CGFloat 130 | let corners: UIRectCorner 131 | 132 | switch roundedCorner { 133 | case .all(let value): 134 | corners = .allCorners 135 | radii = value 136 | case .top(let value): 137 | corners = [.topLeft, .topRight] 138 | radii = value 139 | case .bottom(let value): 140 | corners = [.bottomLeft, .bottomRight] 141 | radii = value 142 | case .none: 143 | return UIBezierPath(rect: rect).cgPath 144 | } 145 | 146 | return UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radii, height: radii)).cgPath 147 | } 148 | 149 | // MARK: - Separator 150 | 151 | fileprivate func reloadSeparator() { 152 | if separatorVisible && backgroundInsets.bottom < separatorHeight { 153 | if separatorView == nil { 154 | let view = UIView() 155 | view.backgroundColor = separatorColor 156 | 157 | addSubview(view) 158 | separatorView = view 159 | } 160 | } 161 | else { 162 | separatorView?.removeFromSuperview() 163 | separatorView = nil 164 | } 165 | } 166 | 167 | // MARK - Background 168 | 169 | fileprivate func reloadBackgroundColor() { 170 | backgroundColor = isHighlighted ? highlightedBackgroundColor : normalBackgroundColor 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Sheet/SheetCollectionViewLayout.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SheetCollectionViewLayout.swift 3 | // ImagePickerSheetController 4 | // 5 | // Created by Laurin Brandner on 26/08/15. 6 | // Copyright © 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SheetCollectionViewLayout: UICollectionViewLayout { 12 | 13 | fileprivate var layoutAttributes = [[UICollectionViewLayoutAttributes]]() 14 | fileprivate var invalidatedLayoutAttributes: [[UICollectionViewLayoutAttributes]]? 15 | fileprivate var contentSize = CGSize.zero 16 | 17 | // MARK: - Layout 18 | 19 | override func prepare() { 20 | super.prepare() 21 | 22 | layoutAttributes.removeAll(keepingCapacity: false) 23 | contentSize = CGSize.zero 24 | 25 | if let collectionView = collectionView, 26 | let delegate = collectionView.delegate as? UICollectionViewDelegateFlowLayout { 27 | let sections = collectionView.numberOfSections 28 | var origin = CGPoint() 29 | 30 | for section in 0 ..< sections { 31 | var sectionAttributes = [UICollectionViewLayoutAttributes]() 32 | let items = collectionView.numberOfItems(inSection: section) 33 | let indexPaths = (0 ..< items).map { IndexPath(item: $0, section: section) } 34 | 35 | for indexPath in indexPaths { 36 | let size = delegate.collectionView?(collectionView, layout: self, sizeForItemAt: indexPath) ?? CGSize.zero 37 | 38 | let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath) 39 | attributes.frame = CGRect(origin: origin, size: size) 40 | 41 | sectionAttributes.append(attributes) 42 | origin.y = attributes.frame.maxY 43 | } 44 | 45 | layoutAttributes.append(sectionAttributes) 46 | } 47 | 48 | contentSize = CGSize(width: collectionView.frame.width, height: origin.y) 49 | } 50 | } 51 | 52 | override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool { 53 | return true 54 | } 55 | 56 | override func invalidateLayout() { 57 | invalidatedLayoutAttributes = layoutAttributes 58 | super.invalidateLayout() 59 | } 60 | 61 | override var collectionViewContentSize : CGSize { 62 | return contentSize 63 | } 64 | 65 | override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? { 66 | return layoutAttributes.reduce([], +) 67 | .filter { rect.intersects($0.frame) } 68 | } 69 | 70 | fileprivate func layoutAttributesForItemAtIndexPath(_ indexPath: IndexPath, allAttributes: [[UICollectionViewLayoutAttributes]]) -> UICollectionViewLayoutAttributes? { 71 | guard allAttributes.count > indexPath.section && allAttributes[indexPath.section].count > indexPath.item else { 72 | return nil 73 | } 74 | 75 | return allAttributes[indexPath.section][indexPath.item] 76 | } 77 | 78 | fileprivate func invalidatedLayoutAttributesForItemAtIndexPath(_ indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 79 | guard let invalidatedLayoutAttributes = invalidatedLayoutAttributes else { 80 | return nil 81 | } 82 | 83 | return layoutAttributesForItemAtIndexPath(indexPath, allAttributes: invalidatedLayoutAttributes) 84 | } 85 | 86 | override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 87 | return layoutAttributesForItemAtIndexPath(indexPath, allAttributes: layoutAttributes) 88 | } 89 | 90 | override func initialLayoutAttributesForAppearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 91 | return invalidatedLayoutAttributesForItemAtIndexPath(itemIndexPath) ?? layoutAttributesForItem(at: itemIndexPath) 92 | } 93 | 94 | override func finalLayoutAttributesForDisappearingItem(at itemIndexPath: IndexPath) -> UICollectionViewLayoutAttributes? { 95 | return layoutAttributesForItem(at: itemIndexPath) 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Sheet/SheetController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SheetController.swift 3 | // ImagePickerSheetController 4 | // 5 | // Created by Laurin Brandner on 27/08/15. 6 | // Copyright © 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | let sheetInset: CGFloat = 10 12 | 13 | class SheetController: NSObject { 14 | 15 | fileprivate(set) lazy var sheetCollectionView: UICollectionView = { 16 | let layout = SheetCollectionViewLayout() 17 | let collectionView = UICollectionView(frame: CGRect(), collectionViewLayout: layout) 18 | collectionView.dataSource = self 19 | collectionView.delegate = self 20 | collectionView.accessibilityIdentifier = "ImagePickerSheet" 21 | collectionView.backgroundColor = .clear 22 | collectionView.alwaysBounceVertical = false 23 | collectionView.register(SheetPreviewCollectionViewCell.self, forCellWithReuseIdentifier: NSStringFromClass(SheetPreviewCollectionViewCell.self)) 24 | collectionView.register(SheetActionCollectionViewCell.self, forCellWithReuseIdentifier: NSStringFromClass(SheetActionCollectionViewCell.self)) 25 | 26 | return collectionView 27 | }() 28 | 29 | var previewCollectionView: PreviewCollectionView 30 | 31 | fileprivate(set) var actions = [ImagePickerAction]() 32 | 33 | var actionHandlingCallback: (() -> ())? 34 | 35 | fileprivate(set) var previewHeight: CGFloat = 0 36 | var numberOfSelectedAssets = 0 37 | 38 | var preferredSheetHeight: CGFloat { 39 | return allIndexPaths().map { self.sizeForSheetItemAtIndexPath($0).height } 40 | .reduce(0, +) 41 | } 42 | 43 | // MARK: - Initialization 44 | 45 | init(previewCollectionView: PreviewCollectionView) { 46 | self.previewCollectionView = previewCollectionView 47 | 48 | super.init() 49 | } 50 | 51 | // MARK: - Data Source 52 | // These methods are necessary so that no call cycles happen when calculating some design attributes 53 | 54 | fileprivate func numberOfSections() -> Int { 55 | return 2 56 | } 57 | 58 | fileprivate func numberOfItemsInSection(_ section: Int) -> Int { 59 | if section == 0 { 60 | return 1 61 | } 62 | 63 | return actions.count 64 | } 65 | 66 | fileprivate func allIndexPaths() -> [IndexPath] { 67 | let s = numberOfSections() 68 | return (0 ..< s).map { (section: Int) -> (Int, Int) in (self.numberOfItemsInSection(section), section) } 69 | .flatMap { (numberOfItems: Int, section: Int) -> [IndexPath] in 70 | (0 ..< numberOfItems).map { (item: Int) -> IndexPath in IndexPath(item: item, section: section) } 71 | } 72 | } 73 | 74 | fileprivate func sizeForSheetItemAtIndexPath(_ indexPath: IndexPath) -> CGSize { 75 | let height: CGFloat = { 76 | if indexPath.section == 0 { 77 | return previewHeight 78 | } 79 | 80 | let actionItemHeight: CGFloat = 57 81 | 82 | let insets = attributesForItemAtIndexPath(indexPath).backgroundInsets 83 | return actionItemHeight + insets.top + insets.bottom 84 | }() 85 | 86 | return CGSize(width: sheetCollectionView.bounds.width, height: height) 87 | } 88 | 89 | // MARK: - Design 90 | 91 | fileprivate func attributesForItemAtIndexPath(_ indexPath: IndexPath) -> (corners: RoundedCorner, backgroundInsets: UIEdgeInsets) { 92 | let cornerRadius: CGFloat = 13 93 | let innerInset: CGFloat = 4 94 | var indexPaths = allIndexPaths() 95 | 96 | guard indexPaths.first != indexPath else { 97 | return (.top(cornerRadius), UIEdgeInsets(top: 0, left: sheetInset, bottom: 0, right: sheetInset)) 98 | } 99 | 100 | let cancelIndexPath = actions.index { $0.style == ImagePickerActionStyle.cancel } 101 | .map { IndexPath(item: $0, section: 1) } 102 | 103 | 104 | if let cancelIndexPath = cancelIndexPath { 105 | if cancelIndexPath == indexPath { 106 | return (.all(cornerRadius), UIEdgeInsets(top: innerInset, left: sheetInset, bottom: sheetInset, right: sheetInset)) 107 | } 108 | 109 | indexPaths.removeLast() 110 | 111 | if indexPath == indexPaths.last { 112 | return (.bottom(cornerRadius), UIEdgeInsets(top: 0, left: sheetInset, bottom: innerInset, right: sheetInset)) 113 | } 114 | } 115 | else if indexPath == indexPaths.last { 116 | return (.bottom(cornerRadius), UIEdgeInsets(top: 0, left: sheetInset, bottom: sheetInset, right: sheetInset)) 117 | } 118 | 119 | return (.none, UIEdgeInsets(top: 0, left: sheetInset, bottom: 0, right: sheetInset)) 120 | } 121 | 122 | fileprivate func fontForAction(_ action: ImagePickerAction) -> UIFont { 123 | if action.style == .cancel { 124 | return UIFont.boldSystemFont(ofSize: 21) 125 | } 126 | return UIFont.systemFont(ofSize: 21) 127 | } 128 | 129 | // MARK: - Actions 130 | 131 | func reloadActionItems() { 132 | sheetCollectionView.reloadSections(IndexSet(integer: 1)) 133 | } 134 | 135 | func addAction(_ action: ImagePickerAction) { 136 | if action.style == .cancel { 137 | actions = actions.filter { $0.style != .cancel } 138 | } 139 | 140 | actions.append(action) 141 | 142 | if let index = actions.index(where: { $0.style == .cancel }) { 143 | let cancelAction = actions.remove(at: index) 144 | actions.append(cancelAction) 145 | } 146 | 147 | reloadActionItems() 148 | } 149 | 150 | func removeAllActions() { 151 | actions = [] 152 | reloadActionItems() 153 | } 154 | 155 | fileprivate func handleAction(_ action: ImagePickerAction) { 156 | actionHandlingCallback?() 157 | action.handle(numberOfSelectedAssets) 158 | } 159 | 160 | @objc func handleCancelAction() { 161 | let cancelAction = actions.filter { $0.style == .cancel } 162 | .first 163 | 164 | if let cancelAction = cancelAction { 165 | handleAction(cancelAction) 166 | } 167 | else { 168 | actionHandlingCallback?() 169 | } 170 | } 171 | 172 | // MARK: - 173 | 174 | func setPreviewHeight(_ height: CGFloat, invalidateLayout: Bool) { 175 | previewHeight = height 176 | if invalidateLayout { 177 | sheetCollectionView.collectionViewLayout.invalidateLayout() 178 | } 179 | } 180 | 181 | } 182 | 183 | extension SheetController: UICollectionViewDataSource { 184 | 185 | func numberOfSections(in collectionView: UICollectionView) -> Int { 186 | return numberOfSections() 187 | } 188 | 189 | func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 190 | return numberOfItemsInSection(section) 191 | } 192 | 193 | func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { 194 | let cell: SheetCollectionViewCell 195 | 196 | if indexPath.section == 0 { 197 | let previewCell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(SheetPreviewCollectionViewCell.self), for: indexPath) as! SheetPreviewCollectionViewCell 198 | previewCell.collectionView = previewCollectionView 199 | 200 | cell = previewCell 201 | } 202 | else { 203 | let action = actions[indexPath.item] 204 | let actionCell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(SheetActionCollectionViewCell.self), for: indexPath) as! SheetActionCollectionViewCell 205 | actionCell.textLabel.font = fontForAction(action) 206 | actionCell.textLabel.text = numberOfSelectedAssets > 0 ? action.secondaryTitle(numberOfSelectedAssets) : action.title 207 | 208 | cell = actionCell 209 | } 210 | 211 | cell.separatorVisible = (indexPath.section == 1) 212 | 213 | // iOS specific design 214 | (cell.roundedCorners, cell.backgroundInsets) = attributesForItemAtIndexPath(indexPath) 215 | cell.normalBackgroundColor = UIColor(white: 0.97, alpha: 1) 216 | cell.highlightedBackgroundColor = UIColor(white: 0.92, alpha: 1) 217 | cell.separatorColor = UIColor(white: 0.84, alpha: 1) 218 | 219 | return cell 220 | } 221 | 222 | } 223 | 224 | extension SheetController: UICollectionViewDelegate { 225 | 226 | func collectionView(_ collectionView: UICollectionView, shouldHighlightItemAt indexPath: IndexPath) -> Bool { 227 | return indexPath.section != 0 228 | } 229 | 230 | func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { 231 | collectionView.deselectItem(at: indexPath, animated: true) 232 | 233 | handleAction(actions[indexPath.item]) 234 | } 235 | 236 | } 237 | 238 | extension SheetController: UICollectionViewDelegateFlowLayout { 239 | 240 | func collectionView(_ collectionView: UICollectionView, layout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { 241 | return sizeForSheetItemAtIndexPath(indexPath) 242 | } 243 | 244 | } 245 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetController/Sheet/SheetPreviewCollectionViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SheetPreviewCollectionViewCell.swift 3 | // ImagePickerSheetController 4 | // 5 | // Created by Laurin Brandner on 06/09/14. 6 | // Copyright (c) 2014 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class SheetPreviewCollectionViewCell: SheetCollectionViewCell { 12 | 13 | var collectionView: PreviewCollectionView? { 14 | willSet { 15 | if let collectionView = collectionView { 16 | collectionView.removeFromSuperview() 17 | } 18 | 19 | if let collectionView = newValue { 20 | addSubview(collectionView) 21 | } 22 | } 23 | } 24 | 25 | // MARK: - Other Methods 26 | 27 | override func prepareForReuse() { 28 | collectionView = nil 29 | } 30 | 31 | // MARK: - Layout 32 | 33 | override func layoutSubviews() { 34 | super.layoutSubviews() 35 | 36 | collectionView?.frame = bounds.inset(by: backgroundInsets) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetControllerTests/ActionHandlingTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ActionHandlingTests.swift 3 | // ImagePickerSheetController 4 | // 5 | // Created by Laurin Brandner on 06/09/15. 6 | // Copyright © 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import KIF 11 | import Nimble 12 | import ImagePickerSheetController 13 | 14 | class ActionHandlingTests: ImagePickerSheetControllerTests { 15 | 16 | var defaultAction: ImagePickerAction! 17 | var cancelAction: ImagePickerAction! 18 | 19 | var defaultActionCalled: Int! 20 | var defaultSecondaryActionCalled: Int! 21 | var cancelActionCalled: Int! 22 | var cancelSecondaryActionCalled: Int! 23 | 24 | override func setUp() { 25 | super.setUp() 26 | 27 | defaultActionCalled = 0 28 | defaultSecondaryActionCalled = 0 29 | cancelActionCalled = 0 30 | cancelSecondaryActionCalled = 0 31 | 32 | defaultAction = ImagePickerAction(title: "Action", handler: { _ in 33 | self.defaultActionCalled = self.defaultActionCalled+1 34 | }, secondaryHandler: { _, _ in 35 | self.defaultSecondaryActionCalled = self.defaultSecondaryActionCalled+1 36 | }) 37 | imageController.addAction(defaultAction) 38 | 39 | cancelAction = ImagePickerAction(title: "Cancel", style: .cancel, handler: { _ in 40 | self.cancelActionCalled = self.cancelActionCalled+1 41 | }, secondaryHandler: { _, _ in 42 | self.cancelSecondaryActionCalled = self.cancelSecondaryActionCalled+1 43 | }) 44 | imageController.addAction(cancelAction) 45 | } 46 | 47 | func testDefaultActionHandling() { 48 | presentImagePickerSheetController() 49 | 50 | tester().tapView(withAccessibilityLabel: defaultAction.title) 51 | 52 | expect(self.defaultActionCalled) == 1 53 | expect(self.defaultSecondaryActionCalled) == 0 54 | expect(self.cancelActionCalled) == 0 55 | expect(self.cancelSecondaryActionCalled) == 0 56 | } 57 | 58 | func testSecondaryActionHandling() { 59 | presentImagePickerSheetController() 60 | 61 | tester().tapImagePreviewAtIndexPath(IndexPath(item: 0, section: 0), inCollectionViewWithAccessibilityIdentifier: imageControllerPreviewIdentifier) 62 | tester().tapView(withAccessibilityLabel: defaultAction.title) 63 | 64 | expect(self.defaultActionCalled) == 0 65 | expect(self.defaultSecondaryActionCalled) == 1 66 | expect(self.cancelActionCalled) == 0 67 | expect(self.cancelSecondaryActionCalled) == 0 68 | } 69 | 70 | func testCancelActionHandlingWhenTappingAction() { 71 | presentImagePickerSheetController() 72 | 73 | tester().tapView(withAccessibilityLabel: cancelAction.title) 74 | 75 | expect(self.defaultActionCalled) == 0 76 | expect(self.defaultSecondaryActionCalled) == 0 77 | expect(self.cancelActionCalled) == 1 78 | expect(self.cancelSecondaryActionCalled) == 0 79 | } 80 | 81 | func testCancelActionHandlingWhenTappingBackground() { 82 | presentImagePickerSheetController() 83 | 84 | tester().tapView(withAccessibilityIdentifier: imageControllerBackgroundViewIdentifier) 85 | 86 | expect(self.defaultActionCalled) == 0 87 | expect(self.defaultSecondaryActionCalled) == 0 88 | expect(self.cancelActionCalled) == 1 89 | expect(self.cancelSecondaryActionCalled) == 0 90 | } 91 | 92 | func testAdaptionOfActionTitles() { 93 | imageController.addAction(ImagePickerAction(title: "Action", secondaryTitle: { "Secondary \($0)" }, handler: { _ in })) 94 | presentImagePickerSheetController() 95 | 96 | let indexPath = IndexPath(item: 0, section: 0) 97 | tester().tapImagePreviewAtIndexPath(indexPath, inCollectionViewWithAccessibilityIdentifier: imageControllerPreviewIdentifier) 98 | 99 | tester().waitForView(withAccessibilityLabel: "Secondary 1") 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetControllerTests/AddingActionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AddingActionTests.swift 3 | // ImagePickerSheetController 4 | // 5 | // Created by Laurin Brandner on 06/09/15. 6 | // Copyright © 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import KIF 11 | import Nimble 12 | import ImagePickerSheetController 13 | 14 | class AddingActionTests: ImagePickerSheetControllerTests { 15 | 16 | func testAddingTwoCancelActions() { 17 | imageController.addAction(ImagePickerAction(title: "Cancel1", style: .cancel, handler: { _ in })) 18 | imageController.addAction(ImagePickerAction(title: "Cancel2", style: .cancel, handler: { _ in })) 19 | 20 | expect(self.imageController.actions.filter { $0.style == .Cancel }.count) == 1 21 | } 22 | 23 | func testDisplayOfAddedActions() { 24 | let actions: [(String, ImagePickerActionStyle)] = [("Action1", .default), 25 | ("Action2", .default), 26 | ("Cancel", .cancel)] 27 | 28 | for (title, style) in actions { 29 | imageController.addAction(ImagePickerAction(title: title, style: style, handler: { _ in })) 30 | } 31 | 32 | presentImagePickerSheetController() 33 | 34 | for (title, _) in actions { 35 | tester().waitForView(withAccessibilityLabel: title) 36 | } 37 | } 38 | 39 | func testActionOrdering() { 40 | imageController.addAction(ImagePickerAction(title: cancelActionTitle, style: .cancel, handler: { _ in })) 41 | imageController.addAction(ImagePickerAction(title: defaultActionTitle, handler: { _ in })) 42 | 43 | expect(self.imageController.actions.map { $0.title }) == [defaultActionTitle, cancelActionTitle] 44 | } 45 | 46 | func testAddingActionAfterPresentation() { 47 | presentImagePickerSheetController() 48 | 49 | imageController.addAction(ImagePickerAction(title: defaultActionTitle, handler: { _ in })) 50 | tester().waitForView(withAccessibilityLabel: defaultActionTitle) 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetControllerTests/DismissalTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DismissalTests.swift 3 | // ImagePickerSheetController 4 | // 5 | // Created by Laurin Brandner on 06/09/15. 6 | // Copyright © 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import KIF 11 | import ImagePickerSheetController 12 | 13 | class DismissalTests: ImagePickerSheetControllerTests { 14 | 15 | override func setUp() { 16 | super.setUp() 17 | 18 | imageController.addAction(ImagePickerAction(title: defaultActionTitle, style: .default, handler: { _ in })) 19 | imageController.addAction(ImagePickerAction(title: cancelActionTitle, style: .cancel, handler: { _ in })) 20 | 21 | presentImagePickerSheetController() 22 | } 23 | 24 | func testDismissalByTappingDefaultAction() { 25 | tester().tapView(withAccessibilityLabel: defaultActionTitle) 26 | tester().waitForAbsenceOfView(withAccessibilityIdentifier: imageControllerViewIdentifier) 27 | } 28 | 29 | func testDismissalByTappingCancelAction() { 30 | tester().tapView(withAccessibilityLabel: cancelActionTitle) 31 | tester().waitForAbsenceOfView(withAccessibilityIdentifier: imageControllerViewIdentifier) 32 | } 33 | 34 | func testDismissalByTappingBackground() { 35 | tester().tapView(withAccessibilityIdentifier: imageControllerBackgroundViewIdentifier) 36 | tester().waitForAbsenceOfView(withAccessibilityIdentifier: imageControllerViewIdentifier) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetControllerTests/ImagePickerSheetControllerTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagePickerSheetControllerTests.swift 3 | // ImagePickerSheetControllerTests 4 | // 5 | // Created by Laurin Brandner on 26/05/15. 6 | // Copyright (c) 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | import KIF 12 | import Nimble 13 | import Photos 14 | import ImagePickerSheetController 15 | 16 | let imageControllerViewIdentifier = "ImagePickerSheet" 17 | let imageControllerBackgroundViewIdentifier = "ImagePickerSheetBackground" 18 | let imageControllerPreviewIdentifier = "ImagePickerSheetPreview" 19 | 20 | class ImagePickerSheetControllerTests: XCTestCase { 21 | 22 | let rootViewController = UIApplication.shared.windows.first!.rootViewController! 23 | var imageController: ImagePickerSheetController! 24 | 25 | let defaultActionTitle = "Action" 26 | let cancelActionTitle = "Cancel" 27 | 28 | // MARK: - Setup 29 | 30 | override func setUp() { 31 | super.setUp() 32 | 33 | imageController = ImagePickerSheetController(mediaType: .imageAndVideo) 34 | } 35 | 36 | override func tearDown() { 37 | super.tearDown() 38 | 39 | rootViewController.dismiss(animated: false, completion: nil) 40 | } 41 | 42 | // MARK: - Utilities 43 | 44 | func presentImagePickerSheetController(_ animated: Bool = false) { 45 | rootViewController.present(imageController, animated: animated, completion: nil) 46 | tester().waitForView(withAccessibilityIdentifier: imageControllerViewIdentifier) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetControllerTests/ImageSelectionTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImageSelectionTests.swift 3 | // ImagePickerSheetController 4 | // 5 | // Created by Laurin Brandner on 06/09/15. 6 | // Copyright © 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import KIF 11 | import Nimble 12 | import Photos 13 | import ImagePickerSheetController 14 | 15 | class ImageSelectionTests: ImagePickerSheetControllerTests { 16 | 17 | let result: PHFetchResult = { () -> PHFetchResult in 18 | let options = PHFetchOptions() 19 | options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] 20 | 21 | return PHAsset.fetchAssets(with: .image, options: options) 22 | }() 23 | 24 | let count = 3 25 | 26 | override func setUp() { 27 | super.setUp() 28 | 29 | presentImagePickerSheetController() 30 | } 31 | 32 | } 33 | 34 | class ImageSelectionWithoutLimitTests: ImageSelectionTests { 35 | 36 | override func setUp() { 37 | super.setUp() 38 | 39 | for i in 0 ..< count { 40 | let indexPath = IndexPath(item: 0, section: i) 41 | tester().tapImagePreviewAtIndexPath(indexPath, inCollectionViewWithAccessibilityIdentifier: imageControllerPreviewIdentifier) 42 | } 43 | 44 | expect(self.imageController.selectedAssets.count) == count 45 | } 46 | 47 | func testImageSelection() { 48 | let selectedAssets = imageController.selectedAssets 49 | result.enumerateObjects { obj, idx, _ in 50 | if let asset = obj as? PHAsset , idx < 3 { 51 | expect(asset.localIdentifier) == selectedAssets[idx].localIdentifier 52 | } 53 | } 54 | } 55 | 56 | func testImageDeselection() { 57 | let indexPath = IndexPath(item: 0, section: 0) 58 | tester().tapImagePreviewAtIndexPath(indexPath, inCollectionViewWithAccessibilityIdentifier: imageControllerPreviewIdentifier) 59 | 60 | expect(self.imageController.selectedAssets.count) == count - 1 61 | 62 | let selectedAssets = imageController.selectedAssets 63 | result.enumerateObjects { obj, idx, _ in 64 | if let asset = obj as? PHAsset , idx < self.count && idx > 0 { 65 | expect(asset.localIdentifier) == selectedAssets[idx-1].localIdentifier 66 | } 67 | } 68 | } 69 | 70 | } 71 | 72 | class ImageSelectionWithLimitTests: ImageSelectionTests { 73 | 74 | func testImageSelection() { 75 | let maxSelection = 2 76 | imageController.maximumSelection = maxSelection 77 | 78 | for i in 0 ..< count { 79 | let indexPath = IndexPath(item: 0, section: i) 80 | tester().tapImagePreviewAtIndexPath(indexPath, inCollectionViewWithAccessibilityIdentifier: imageControllerPreviewIdentifier) 81 | } 82 | 83 | expect(self.imageController.selectedAssets.count) == maxSelection 84 | 85 | let selectedAssets = imageController.selectedAssets 86 | result.enumerateObjects { obj, idx, _ in 87 | if let asset = obj as? PHAsset , idx < maxSelection && idx > 0 { 88 | expect(asset.localIdentifier) == selectedAssets[idx-1].localIdentifier 89 | } 90 | } 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetControllerTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetControllerTests/KIFExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KIFExtensions.swift 3 | // ImagePickerSheetController 4 | // 5 | // Created by Laurin Brandner on 05/06/15. 6 | // Copyright (c) 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import KIF 11 | 12 | extension XCTestCase { 13 | 14 | func tester(_ file : String = #file, _ line : Int = #line) -> KIFUITestActor { 15 | return KIFUITestActor(inFile: file, atLine: line, delegate: self) 16 | } 17 | 18 | func system(_ file : String = #file, _ line : Int = #line) -> KIFSystemTestActor { 19 | return KIFSystemTestActor(inFile: file, atLine: line, delegate: self) 20 | } 21 | 22 | } 23 | 24 | extension KIFTestActor { 25 | 26 | func tester(_ file : String = #file, _ line : Int = #line) -> KIFUITestActor { 27 | return KIFUITestActor(inFile: file, atLine: line, delegate: self) 28 | } 29 | 30 | func system(_ file : String = #file, _ line : Int = #line) -> KIFSystemTestActor { 31 | return KIFSystemTestActor(inFile: file, atLine: line, delegate: self) 32 | } 33 | 34 | } 35 | 36 | extension KIFUITestActor { 37 | 38 | // Needed because UICollectionView fails to select an item due to a reason I don't quite grasp 39 | func tapImagePreviewAtIndexPath(_ indexPath: IndexPath, inCollectionViewWithAccessibilityIdentifier collectionViewIdentifier: String) { 40 | let collectionView = waitForView(withAccessibilityIdentifier: collectionViewIdentifier) as! UICollectionView 41 | 42 | let cellAttributes = collectionView.layoutAttributesForItem(at: indexPath) 43 | let contentOffset = CGPoint(x: cellAttributes!.frame.minX-collectionView.contentInset.left, y: 0) 44 | 45 | collectionView.setContentOffset(contentOffset, animated: false) 46 | 47 | let newCellAttributes = collectionView.layoutAttributesForItem(at: indexPath) 48 | let cellCenter = collectionView.convert(newCellAttributes!.center, to: nil) 49 | 50 | // Tap it manually here, no UICollectionView selection 51 | tapScreen(at: cellCenter) 52 | 53 | // Wait so that a possible preview zooming animation can finish 54 | waitForAnimationsToFinish() 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /ImagePickerSheetController/ImagePickerSheetControllerTests/PresentationTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PresentationTests.swift 3 | // ImagePickerSheetController 4 | // 5 | // Created by Laurin Brandner on 06/09/15. 6 | // Copyright © 2015 Laurin Brandner. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import KIF 11 | 12 | class PresentationTests: ImagePickerSheetControllerTests { 13 | 14 | func testPresentation() { 15 | presentImagePickerSheetController(true) 16 | tester().acknowledgeSystemAlert() 17 | tester().waitForView(withAccessibilityIdentifier: imageControllerViewIdentifier) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 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 | # ImagePickerSheetController 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/ImagePickerSheetController/blob/master/LICENSE) 5 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 6 | 7 | ## About 8 | ImagePickerSheetController is a component that replicates the custom photo action sheet in iMessage. It's very similar to UIAlertController which makes its usage simple and concise. 9 | ⚠️You can also find an iOS 10 version of this library [here](https://github.com/lbrndnr/ImagePickerTrayController)⚠️ 10 | 11 | ![Screenshot](https://raw.githubusercontent.com/lbrndnr/ImagePickerSheetController/master/Screenshots/GoT.gif) 12 | 13 | ## Usage 14 | `ImagePickerSheetController` is similar to `UIAlertController` in its usage. 15 | 16 | ### Example 17 | 18 | ```swift 19 | let controller = ImagePickerSheetController(mediaType: .ImageAndVideo) 20 | controller.addAction(ImagePickerAction(title: NSLocalizedString("Take Photo Or Video", comment: "Action Title"), secondaryTitle: NSLocalizedString("Add comment", comment: "Action Title"), handler: { _ in 21 | presentImagePickerController(.Camera) 22 | }, secondaryHandler: { _, numberOfPhotos in 23 | println("Comment \(numberOfPhotos) photos") 24 | })) 25 | controller.addAction(ImagePickerAction(title: NSLocalizedString("Photo Library", comment: "Action Title"), secondaryTitle: { NSString.localizedStringWithFormat(NSLocalizedString("ImagePickerSheet.button1.Send %lu Photo", comment: "Action Title"), $0) as String}, handler: { _ in 26 | presentImagePickerController(.PhotoLibrary) 27 | }, secondaryHandler: { _, numberOfPhotos in 28 | println("Send \(controller.selectedImageAssets)") 29 | })) 30 | controller.addAction(ImagePickerAction(title: NSLocalizedString("Cancel", comment: "Action Title"), style: .Cancel, handler: { _ in 31 | println("Cancelled") 32 | })) 33 | 34 | presentViewController(controller, animated: true, completion: nil) 35 | ``` 36 | It's recommended to use [stringsdict](https://developer.apple.com/library/ios/documentation/MacOSX/Conceptual/BPInternational/StringsdictFileFormat/StringsdictFileFormat.html) to easily translate plural forms in any language. 37 | 38 | ## Installation 39 | 40 | ### CocoaPods 41 | ```ruby 42 | pod "ImagePickerSheetController", "~> 0.9.1" 43 | ``` 44 | 45 | ###Carthage 46 | ```objc 47 | github "lbrndnr/ImagePickerSheetController" ~> 0.9.1 48 | ``` 49 | 50 | You should also add two new values to your app's `Info.plist` to tell the user why you need to access the Camera and Photo Library. 51 | ``` 52 | NSCameraUsageDescription 53 | Camera usage description 54 | NSPhotoLibraryUsageDescription 55 | Photo Library usage description 56 | ``` 57 | 58 | ## Requirements 59 | ImagePickerSheetController is written in Swift and links against `Photos.framework`. It therefore requires iOS 9.0 or later. 60 | 61 | ## Author 62 | I'm Laurin Brandner, I'm on [Twitter](https://twitter.com/lbrndnr). 63 | 64 | ## License 65 | ImagePickerSheetController is licensed under the [MIT License](http://opensource.org/licenses/mit-license.php). 66 | -------------------------------------------------------------------------------- /Screenshots/GoT.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerSheetController/c57fa10ad847e29cabfa90a60baffca3c67474b8/Screenshots/GoT.gif -------------------------------------------------------------------------------- /Screenshots/Nature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbrndnr/ImagePickerSheetController/c57fa10ad847e29cabfa90a60baffca3c67474b8/Screenshots/Nature.png --------------------------------------------------------------------------------