├── AAImagePickerController ├── check.png ├── take_photo.png └── AAImagePickerController.swift ├── AAImagePickerControllerDemo.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── .gitignore ├── AAImagePickerControllerDemoTests ├── Info.plist └── AAImagePickerControllerDemoTests.swift ├── AAImagePickerControllerDemo ├── Images.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── ViewController.swift ├── AppDelegate.swift └── Base.lproj │ ├── LaunchScreen.xib │ └── Main.storyboard ├── README.md └── AKPickerView.swift /AAImagePickerController/check.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anas10/AAImagePickerController/HEAD/AAImagePickerController/check.png -------------------------------------------------------------------------------- /AAImagePickerController/take_photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anas10/AAImagePickerController/HEAD/AAImagePickerController/take_photo.png -------------------------------------------------------------------------------- /AAImagePickerControllerDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Swift ### 2 | # Xcode 3 | # 4 | build/ 5 | *.pbxuser 6 | !default.pbxuser 7 | *.mode1v3 8 | !default.mode1v3 9 | *.mode2v3 10 | !default.mode2v3 11 | *.perspectivev3 12 | !default.perspectivev3 13 | xcuserdata 14 | *.xccheckout 15 | *.moved-aside 16 | DerivedData 17 | *.hmap 18 | *.ipa 19 | *.xcuserstate 20 | -------------------------------------------------------------------------------- /AAImagePickerControllerDemoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | co.aitali.$(PRODUCT_NAME:rfc1034identifier) 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 | -------------------------------------------------------------------------------- /AAImagePickerControllerDemoTests/AAImagePickerControllerDemoTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AAImagePickerControllerDemoTests.swift 3 | // AAImagePickerControllerDemoTests 4 | // 5 | // Created by Anas perso on 16/05/15. 6 | // Copyright (c) 2015 Anas. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import XCTest 11 | 12 | class AAImagePickerControllerDemoTests: XCTestCase { 13 | 14 | override func setUp() { 15 | super.setUp() 16 | // Put setup code here. This method is called before the invocation of each test method in the class. 17 | } 18 | 19 | override func tearDown() { 20 | // Put teardown code here. This method is called after the invocation of each test method in the class. 21 | super.tearDown() 22 | } 23 | 24 | func testExample() { 25 | // This is an example of a functional test case. 26 | XCTAssert(true, "Pass") 27 | } 28 | 29 | func testPerformanceExample() { 30 | // This is an example of a performance test case. 31 | self.measureBlock() { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /AAImagePickerControllerDemo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /AAImagePickerControllerDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | co.aitali.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UIRequiredDeviceCapabilities 30 | 31 | armv7 32 | 33 | UISupportedInterfaceOrientations 34 | 35 | UIInterfaceOrientationPortrait 36 | UIInterfaceOrientationLandscapeLeft 37 | UIInterfaceOrientationLandscapeRight 38 | 39 | UISupportedInterfaceOrientations~ipad 40 | 41 | UIInterfaceOrientationPortrait 42 | UIInterfaceOrientationPortraitUpsideDown 43 | UIInterfaceOrientationLandscapeLeft 44 | UIInterfaceOrientationLandscapeRight 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /AAImagePickerControllerDemo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // AAImagePickerControllerDemo 4 | // 5 | // Created by Anas perso on 16/05/15. 6 | // Copyright (c) 2015 Anas. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | class ViewController: UIViewController { 12 | 13 | @IBOutlet weak var scrollView: UIScrollView! 14 | 15 | override func viewDidLoad() { 16 | super.viewDidLoad() 17 | // Do any additional setup after loading the view, typically from a nib. 18 | } 19 | 20 | override func didReceiveMemoryWarning() { 21 | super.didReceiveMemoryWarning() 22 | // Dispose of any resources that can be recreated. 23 | } 24 | 25 | @IBAction func showPicker(sender: AnyObject) { 26 | let pc = AAImagePickerController() 27 | pc.pickerDelegate = self 28 | pc.allowsMultipleSelection = true 29 | pc.maximumNumberOfSelection = 6 30 | pc.numberOfColumnInPortrait = 4 31 | pc.numberOfColumnInLandscape = 7 32 | pc.showTakePhoto = true 33 | self.presentViewController(pc, animated: true, completion: nil) 34 | } 35 | } 36 | 37 | extension ViewController : AAImagePickerControllerDelegate { 38 | func imagePickerControllerDidCancel() { 39 | self.dismissViewControllerAnimated(true, completion: nil) 40 | } 41 | 42 | func imagePickerControllerDidFinishSelection(images: [ImageItem]) { 43 | self.dismissViewControllerAnimated(true, completion: nil) 44 | println("\(images.count) selected items") 45 | scrollView.subviews.map({ $0.removeFromSuperview() }) 46 | for (index, item) in enumerate(images) { 47 | let imageHeight: CGFloat = scrollView.bounds.height / 2 48 | let imageView = UIImageView(image: item.image) 49 | imageView.contentMode = UIViewContentMode.ScaleAspectFit 50 | imageView.frame = CGRect(x: 0, y: CGFloat(index) * imageHeight, width: scrollView.bounds.width, height: imageHeight) 51 | scrollView.addSubview(imageView) 52 | 53 | } 54 | scrollView.contentSize.height = CGRectGetMaxY((scrollView.subviews.last as! UIView).frame) 55 | } 56 | } 57 | 58 | -------------------------------------------------------------------------------- /AAImagePickerControllerDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // AAImagePickerControllerDemo 4 | // 5 | // Created by Anas perso on 16/05/15. 6 | // Copyright (c) 2015 Anas. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | 17 | func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { 18 | // Override point for customization after application launch. 19 | return true 20 | } 21 | 22 | func applicationWillResignActive(application: UIApplication) { 23 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 24 | // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. 25 | } 26 | 27 | func applicationDidEnterBackground(application: UIApplication) { 28 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 29 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 30 | } 31 | 32 | func applicationWillEnterForeground(application: UIApplication) { 33 | // Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background. 34 | } 35 | 36 | func applicationDidBecomeActive(application: UIApplication) { 37 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 38 | } 39 | 40 | func applicationWillTerminate(application: UIApplication) { 41 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 42 | } 43 | 44 | 45 | } 46 | 47 | -------------------------------------------------------------------------------- /AAImagePickerControllerDemo/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 | -------------------------------------------------------------------------------- /AAImagePickerControllerDemo/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AAImagePickerController 2 | Custom ImagePickerController with camera option, multiselect and quick album selection 3 | 4 | *Screenshots comming soon* 5 | 6 | 7 | ## Features 8 | 9 | - Allows multiple selection of photos 10 | - Allows to take a picture 11 | - Allow easy selection of an album 12 | - Customizable (number of columns, maximumSelection, etc.) 13 | - Supports both portrait mode and landscape mode 14 | - Fast and memory-efficient scrolling 15 | - Compatible with all iPhone and iPad (Tested from iOS 7.0) 16 | 17 | ## Example 18 | 19 | let pc = AAImagePickerController() 20 | pc.pickerDelegate = self 21 | pc.allowsMultipleSelection = true // Default value is true 22 | pc.maximumNumberOfSelection = 6 // Default value is 0 (unlimited) 23 | pc.numberOfColumnInPortrait = 5 // Default value is 4 24 | pc.numberOfColumnInLandscape = 10 // Default value is 7 25 | 26 | self.presentViewController(pc, animated: true, completion: nil) 27 | 28 | 29 | ## Installation 30 | 31 | ### CocoaPods 32 | 33 | *Comming soon* 34 | 35 | ### Carthage 36 | 37 | *Comming soon* 38 | 39 | ### Manual 40 | 41 | 1. Add `AAImagePickerController.swift`, `check.png`, `take_photo.png` and `AKPickerView.swift` to your project 42 | 2. That's all you can use it! 43 | 44 | ## Usage 45 | 46 | ### Basic 47 | 48 | 1. Implement `AAImagePickerControllerDelegate` methods 49 | 2. Initialize a new `AAImagePickerController` object 50 | 3. Set `self` to the `delegate` property 51 | 4. Show the picker by using `presentViewController:animated:completion:` 52 | ``` 53 | let pc = AAImagePickerController() 54 | pc.pickerDelegate = self 55 | 56 | self.presentViewController(pc, animated: true, completion: nil) 57 | ``` 58 | 59 | ### Delegate Methods 60 | 61 | #### Getting the selected items 62 | 63 | Implement `imagePickerControllerDidFinishSelection` to get the items selected by the user. 64 | This method will be called when the user finishes selecting images. 65 | 66 | func imagePickerControllerDidFinishSelection(images: [ImageItem]) { 67 | for (index, item) in enumerate(images) { 68 | let imageView = UIImageView(image: item.image) 69 | ... 70 | } 71 | self.dismissViewControllerAnimated(true, completion: nil) 72 | } 73 | 74 | 75 | #### Getting notified when the user cancels 76 | 77 | Implement `imagePickerControllerDidCancel` to get notified when the user hits "Cancel" button. 78 | 79 | func imagePickerControllerDidCancel() { 80 | self.dismissViewControllerAnimated(true, completion: nil) 81 | } 82 | 83 | ### Customization 84 | 85 | #### Selection mode 86 | 87 | When `allowsMultipleSelection` is `true`, the user can select multiple photos. 88 | The default value is `true`. 89 | 90 | pc.allowsMultipleSelection = true 91 | 92 | You can limit the number of selection `maximumNumberOfSelection` property. 93 | The default value is `0`, which means the number of selection is unlimited. 94 | 95 | pc.maximumNumberOfSelection = 6 96 | 97 | #### Number of columns 98 | 99 | Use `numberOfColumnInPortrait` and `numberOfColumnInLandscape` to change the number of columns in the specified orientation. 100 | The code below shows the default value. 101 | 102 | pc.numberOfColumnInPortrait = 4 103 | pc.numberOfColumnInLandscape = 7 104 | 105 | 106 | ## License 107 | 108 | The MIT License (MIT) 109 | 110 | Copyright (c) 2015 Anas AIT-ALI 111 | 112 | Permission is hereby granted, free of charge, to any person obtaining a copy 113 | of this software and associated documentation files (the "Software"), to deal 114 | in the Software without restriction, including without limitation the rights 115 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 116 | copies of the Software, and to permit persons to whom the Software is 117 | furnished to do so, subject to the following conditions: 118 | 119 | The above copyright notice and this permission notice shall be included in all 120 | copies or substantial portions of the Software. 121 | 122 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 123 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 124 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 125 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 126 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 127 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 128 | SOFTWARE. 129 | -------------------------------------------------------------------------------- /AAImagePickerController/AAImagePickerController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AAImagePickerController.swift 3 | // AAImagePickerControllerDemo 4 | // 5 | // Created by Anas perso on 16/05/15. 6 | // Copyright (c) 2015 Anas. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AssetsLibrary 11 | 12 | // MARK: - Constants 13 | let AAImageCellIdentifier = "AAImageCellIdentifier" 14 | let AATakePhotoCellIdentifier = "AATakePhotoCellIdentifier" 15 | 16 | // MARK: - AAImagePickerControllerDelegate 17 | protocol AAImagePickerControllerDelegate : NSObjectProtocol { 18 | func imagePickerControllerDidFinishSelection(images: [ImageItem]) 19 | func imagePickerControllerDidCancel() 20 | } 21 | 22 | // MARK: - Models 23 | class ImageGroup { 24 | var name: String! 25 | var group: ALAssetsGroup! 26 | } 27 | 28 | class ImageItem : NSObject { 29 | private var originalAsset: ALAsset? 30 | var thumbnailImage: UIImage? 31 | lazy var image: UIImage? = { 32 | if let origAsset = self.originalAsset { 33 | return self.fullScreenImage 34 | } else { 35 | return self.image 36 | } 37 | }() 38 | lazy var fullScreenImage: UIImage? = { 39 | return UIImage(CGImage: self.originalAsset?.defaultRepresentation().fullScreenImage().takeUnretainedValue()) 40 | }() 41 | lazy var fullResolutionImage: UIImage? = { 42 | return UIImage(CGImage: self.originalAsset?.defaultRepresentation().fullResolutionImage().takeUnretainedValue()) 43 | }() 44 | var url: NSURL? 45 | 46 | override func isEqual(object: AnyObject?) -> Bool { 47 | let other = object as! ImageItem 48 | if let url = self.url, otherUrl = other.url { 49 | return url.isEqual(otherUrl) 50 | } 51 | return false 52 | } 53 | } 54 | 55 | // MARK: - AAImagePickerController 56 | class AAImagePickerController : UINavigationController { 57 | 58 | internal weak var pickerDelegate : AAImagePickerControllerDelegate? 59 | var listController : AAImagePickerControllerList! 60 | var allowsMultipleSelection : Bool = true 61 | var maximumNumberOfSelection : Int = 0 62 | var numberOfColumnInPortrait : Int = 4 63 | var numberOfColumnInLandscape : Int = 7 64 | var showTakePhoto : Bool = true 65 | var selectionColor = UIColor.clearColor() { 66 | didSet { 67 | self.listController.selectionColor = selectionColor 68 | } 69 | } 70 | internal var selectedItems = [ImageItem]() { 71 | willSet(newValue) { 72 | let currentCount = selectedItems.count 73 | println("selectedItems = \(currentCount)") 74 | if newValue.count == 0 { 75 | addBtn.title = "Add" 76 | addBtn.enabled = false 77 | } else if (newValue.count != currentCount) { 78 | addBtn.title = "Add (\(newValue.count))" 79 | addBtn.enabled = true 80 | } 81 | } 82 | } 83 | lazy internal var addBtn : UIBarButtonItem = { 84 | let btn : UIBarButtonItem = UIBarButtonItem(title: "Add", style: .Done, target: self, action: "addAction") 85 | btn.enabled = false 86 | return btn 87 | }() 88 | 89 | // MARK: Initialization 90 | convenience init() { 91 | let aListController = AAImagePickerControllerList() 92 | self.init(rootViewController: aListController) 93 | listController = aListController 94 | } 95 | 96 | // MARK: View lifecycle 97 | override func viewDidLoad() { 98 | super.viewDidLoad() 99 | } 100 | 101 | // MARK: UINavigationController 102 | override func pushViewController(viewController: UIViewController, animated: Bool) { 103 | super.pushViewController(viewController, animated: animated) 104 | 105 | self.topViewController.navigationItem.rightBarButtonItem = addBtn 106 | 107 | if self.viewControllers.count == 1 && 108 | self.topViewController?.navigationItem.leftBarButtonItem == nil { 109 | self.topViewController.navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: UIBarButtonSystemItem.Cancel, target: self, action: "cancelAction") 110 | } 111 | } 112 | 113 | // MARK: Delegate 114 | func cancelAction() { 115 | if let aDelegate = self.pickerDelegate { 116 | aDelegate.imagePickerControllerDidCancel() 117 | } 118 | } 119 | 120 | func addAction() { 121 | if let delegate = self.pickerDelegate { 122 | delegate.imagePickerControllerDidFinishSelection(selectedItems) 123 | } 124 | } 125 | } 126 | 127 | internal extension UIViewController { 128 | var imagePickerController: AAImagePickerController? { 129 | get { 130 | let nav = self.navigationController 131 | if nav is AAImagePickerController { 132 | return nav as? AAImagePickerController 133 | } else { 134 | return nil 135 | } 136 | } 137 | } 138 | } 139 | 140 | // MARK : AACollectionViewFlowLayout 141 | internal class AACollectionViewFlowLayout : UICollectionViewFlowLayout { 142 | static let interval: CGFloat = 1 143 | 144 | func commonInit() { 145 | self.minimumInteritemSpacing = AACollectionViewFlowLayout.interval 146 | self.minimumLineSpacing = AACollectionViewFlowLayout.interval 147 | } 148 | 149 | override init() { 150 | super.init() 151 | self.commonInit() 152 | } 153 | 154 | required init(coder aDecoder: NSCoder) { 155 | super.init(coder: aDecoder) 156 | self.commonInit() 157 | } 158 | } 159 | 160 | // MARK: - AAImagePickerControllerList 161 | class AAImagePickerControllerList : UICollectionViewController { 162 | 163 | lazy private var albumPickerView = AKPickerView() 164 | lazy private var library: ALAssetsLibrary = { 165 | return ALAssetsLibrary() 166 | }() 167 | lazy private var groups: NSMutableArray = { 168 | return NSMutableArray() 169 | }() 170 | private lazy var imageItems: NSMutableArray = { 171 | return NSMutableArray() 172 | }() 173 | var selectionColor = UIColor(red: 55/255, green: 93/255, blue: 129/255, alpha: 1.0) 174 | var currentGroupSelection : Int? 175 | var accessDeniedView: UIView = { 176 | let label = UILabel() 177 | label.text = "This application doesn't have access to your photos" 178 | label.textAlignment = NSTextAlignment.Center 179 | label.textColor = UIColor.lightGrayColor() 180 | label.numberOfLines = 0 181 | return label 182 | }() 183 | 184 | // MARK: Initialization 185 | override init(collectionViewLayout layout: UICollectionViewLayout) { 186 | super.init(collectionViewLayout: layout) 187 | } 188 | 189 | convenience init() { 190 | let layout = AACollectionViewFlowLayout() 191 | self.init(collectionViewLayout: layout) 192 | } 193 | 194 | required init(coder aDecoder: NSCoder) { 195 | fatalError("init(coder:) has not been implemented") 196 | } 197 | 198 | // MARK: View lifecycle 199 | override func viewDidLoad() { 200 | super.viewDidLoad() 201 | 202 | albumPickerInitialisation() 203 | 204 | collectionView!.backgroundColor = UIColor.whiteColor() 205 | collectionView!.allowsMultipleSelection = imagePickerController!.allowsMultipleSelection 206 | collectionView!.registerClass(AAImagePickerCollectionCell.self, forCellWithReuseIdentifier: AAImageCellIdentifier) 207 | collectionView!.registerClass(AATakePhotoCollectionCell.self, forCellWithReuseIdentifier: AATakePhotoCellIdentifier) 208 | } 209 | 210 | override func viewDidAppear(animated: Bool) { 211 | super.viewDidAppear(animated) 212 | 213 | updateGroups { () -> () in 214 | self.updateImagesList() 215 | } 216 | } 217 | 218 | // MARK: Library methods 219 | func updateGroups(callback: () -> ()) { 220 | library.enumerateGroupsWithTypes(ALAssetsGroupAll, usingBlock: { (group: ALAssetsGroup!, stop: UnsafeMutablePointer) -> Void in 221 | if group != nil { 222 | if group.numberOfAssets() != 0 { 223 | let groupName = group.valueForProperty(ALAssetsGroupPropertyName) as! String 224 | let assetGroup = ImageGroup() 225 | assetGroup.name = groupName 226 | assetGroup.group = group 227 | self.groups.insertObject(assetGroup, atIndex: 0) 228 | } 229 | } else { 230 | self.albumPickerView.reloadData() 231 | callback() 232 | } 233 | }, failureBlock: { (error: NSError!) -> Void in 234 | self.accessDeniedView.frame = self.collectionView!.bounds 235 | self.collectionView!.addSubview(self.accessDeniedView) 236 | }) 237 | } 238 | 239 | func updateImagesList() { 240 | let selectedAlbum = self.albumPickerView.selectedItem 241 | let currentGroup = groups[selectedAlbum] as! ImageGroup 242 | 243 | self.imageItems.removeAllObjects() 244 | currentGroup.group.setAssetsFilter(ALAssetsFilter.allPhotos()) 245 | currentGroup.group.enumerateAssetsUsingBlock { (result: ALAsset!, index: Int, stop: UnsafeMutablePointer) -> Void in 246 | if result != nil { 247 | let item = ImageItem() 248 | item.thumbnailImage = UIImage(CGImage:result.thumbnail()?.takeUnretainedValue()) 249 | item.url = result.valueForProperty(ALAssetPropertyAssetURL) as? NSURL 250 | item.originalAsset = result 251 | self.imageItems.insertObject(item, atIndex: 0) 252 | } else { 253 | self.collectionView!.reloadData() 254 | } 255 | } 256 | } 257 | 258 | // MARK: UICollectionViewDataSource 259 | override func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { 260 | return 1 261 | } 262 | 263 | override func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 264 | if imageItems.count > 0 { 265 | return imageItems.count + (imagePickerController?.showTakePhoto == true ? 1 : 0) 266 | } 267 | return 0 268 | } 269 | 270 | override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { 271 | if indexPath.row == 0 && imagePickerController?.showTakePhoto == true { 272 | let takePhotoCell = collectionView.dequeueReusableCellWithReuseIdentifier(AATakePhotoCellIdentifier, forIndexPath: indexPath) as! AATakePhotoCollectionCell 273 | animateCell(takePhotoCell, pos: indexPath.row) 274 | return takePhotoCell 275 | } else { 276 | let item = imageItems[indexPath.row - (imagePickerController?.showTakePhoto == true ? 1 : 0)] as! ImageItem 277 | let cell = collectionView.dequeueReusableCellWithReuseIdentifier(AAImageCellIdentifier, forIndexPath: indexPath) as! AAImagePickerCollectionCell 278 | cell.thumbnail = item.thumbnailImage 279 | cell.selectionColor = selectionColor 280 | if find(imagePickerController!.selectedItems, item) != nil { 281 | cell.selected = true 282 | collectionView.selectItemAtIndexPath(indexPath, animated: false, scrollPosition: .None) 283 | } else { 284 | cell.selected = false 285 | collectionView.deselectItemAtIndexPath(indexPath, animated: false) 286 | } 287 | animateCell(cell, pos: indexPath.row) 288 | return cell 289 | } 290 | } 291 | 292 | func animateCell(cell: UICollectionViewCell, pos: Int) { 293 | cell.transform = CGAffineTransformMakeScale(0, 0) 294 | UIView.animateWithDuration(0.3, delay: Double(pos) * 0.01, options: UIViewAnimationOptions.CurveEaseInOut, 295 | animations: { () -> Void in 296 | cell.transform = CGAffineTransformMakeScale(1, 1) 297 | }, completion: nil) 298 | } 299 | 300 | // MARK: UICollectionViewDelegate 301 | override func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool { 302 | let max = imagePickerController!.maximumNumberOfSelection 303 | return max > 0 ? (imagePickerController!.selectedItems.count < max) : true 304 | } 305 | 306 | override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { 307 | if indexPath.row == 0 && imagePickerController?.showTakePhoto == true { 308 | collectionView.deselectItemAtIndexPath(indexPath, animated: false) 309 | if UIImagePickerController.isSourceTypeAvailable(.Camera) { 310 | var picker = UIImagePickerController() 311 | picker.delegate = self 312 | picker.allowsEditing = true 313 | picker.sourceType = UIImagePickerControllerSourceType.Camera 314 | self.presentViewController(picker, animated: true, completion: nil) 315 | } else { 316 | let alert = UIAlertView(title: "Error", message: "This device has no camera", delegate: nil, cancelButtonTitle: "Ok") 317 | alert.show() 318 | } 319 | } else { 320 | let item = imageItems[indexPath.row - (imagePickerController?.showTakePhoto == true ? 1 : 0)] as! ImageItem 321 | if find(imagePickerController!.selectedItems, item) == nil { 322 | imagePickerController!.selectedItems.append(item) 323 | } 324 | } 325 | } 326 | 327 | override func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath) { 328 | if indexPath.row != 0 { 329 | let item = imageItems[indexPath.row - (imagePickerController?.showTakePhoto == true ? 1 : 0)] as! ImageItem 330 | imagePickerController!.selectedItems.removeAtIndex(find(imagePickerController!.selectedItems, item)!) 331 | } 332 | } 333 | 334 | // MARK : Rotation 335 | override func willRotateToInterfaceOrientation(toInterfaceOrientation: UIInterfaceOrientation, duration: NSTimeInterval) { 336 | super.willRotateToInterfaceOrientation(toInterfaceOrientation, duration: duration) 337 | self.collectionView!.collectionViewLayout.invalidateLayout() 338 | } 339 | } 340 | 341 | extension AAImagePickerControllerList : UICollectionViewDelegateFlowLayout { 342 | func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { 343 | let numberOfColumns : Int 344 | let side : CGFloat 345 | if UIInterfaceOrientationIsPortrait(UIApplication.sharedApplication().statusBarOrientation) { 346 | numberOfColumns = imagePickerController!.numberOfColumnInPortrait 347 | } else { 348 | numberOfColumns = imagePickerController!.numberOfColumnInLandscape 349 | } 350 | side = (CGRectGetWidth(collectionView.frame) - AACollectionViewFlowLayout.interval * CGFloat(numberOfColumns - 1)) / CGFloat(numberOfColumns) 351 | return CGSizeMake(side, side) 352 | } 353 | } 354 | 355 | extension AAImagePickerControllerList : UIImagePickerControllerDelegate, UINavigationControllerDelegate { 356 | func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [NSObject : AnyObject]) { 357 | let item = ImageItem() 358 | item.image = info[UIImagePickerControllerEditedImage] as? UIImage 359 | item.url = info[UIImagePickerControllerReferenceURL] as? NSURL 360 | imagePickerController!.selectedItems = [item] 361 | imagePickerController!.addAction() 362 | picker.dismissViewControllerAnimated(true, completion:nil) 363 | } 364 | 365 | func imagePickerControllerDidCancel(picker: UIImagePickerController) { 366 | picker.dismissViewControllerAnimated(true, completion: nil) 367 | } 368 | } 369 | 370 | extension AAImagePickerControllerList : AKPickerViewDelegate, AKPickerViewDataSource { 371 | 372 | func albumPickerInitialisation() { 373 | self.albumPickerView.delegate = self 374 | self.albumPickerView.dataSource = self 375 | self.albumPickerView.font = UIFont(name: "HelveticaNeue-Light", size: 20)! 376 | self.albumPickerView.highlightedFont = UIFont(name: "HelveticaNeue", size: 20)! 377 | self.albumPickerView.interitemSpacing = 10.0 378 | self.albumPickerView.viewDepth = 1000.0 379 | self.albumPickerView.pickerViewStyle = .Wheel 380 | self.albumPickerView.maskDisabled = false 381 | self.albumPickerView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight 382 | if self.navigationController != nil { 383 | self.albumPickerView.frame = self.navigationController!.navigationBar.bounds 384 | } 385 | self.navigationItem.titleView = self.albumPickerView 386 | } 387 | 388 | func numberOfItemsInPickerView(pickerView: AKPickerView) -> Int { 389 | return groups.count 390 | } 391 | 392 | func pickerView(pickerView: AKPickerView, titleForItem item: Int) -> String { 393 | let assetGroup : ImageGroup = groups[item] as! ImageGroup 394 | return assetGroup.name 395 | } 396 | 397 | func pickerView(pickerView: AKPickerView, didSelectItem item: Int) { 398 | if currentGroupSelection == nil || currentGroupSelection != item { 399 | currentGroupSelection = item 400 | updateImagesList() 401 | } 402 | } 403 | } 404 | 405 | // MARK: - UIImage extension 406 | extension UIImage { 407 | func imageWithColor(color: UIColor) -> UIImage { 408 | let rect = CGRectMake(0, 0, self.size.width, self.size.height) 409 | UIGraphicsBeginImageContext(rect.size) 410 | let context = UIGraphicsGetCurrentContext() 411 | color.set() 412 | CGContextTranslateCTM(context, 0, self.size.height) 413 | CGContextScaleCTM(context, 1.0, -1.0) 414 | CGContextClipToMask(context, rect, self.CGImage) 415 | CGContextFillRect(context, rect) 416 | let image = UIGraphicsGetImageFromCurrentImageContext() 417 | UIGraphicsEndImageContext() 418 | return image 419 | } 420 | } 421 | 422 | // MARK: - AAImagePickerCollectionCell 423 | class AAImagePickerCollectionCell: UICollectionViewCell { 424 | private var imageView = UIImageView() 425 | private var selectedView = UIImageView(image: UIImage(named: "check")) 426 | private var overlay = UIView() 427 | 428 | var selectionColor = UIColor.clearColor() { 429 | didSet { 430 | layer.borderColor = selectionColor.CGColor 431 | // selectedView.image = selectedView.image?.imageWithColor(selectionColor) 432 | } 433 | } 434 | 435 | var thumbnail: UIImage! { 436 | didSet { 437 | self.imageView.image = thumbnail 438 | } 439 | } 440 | 441 | override var selected: Bool { 442 | didSet { 443 | selectedView.hidden = !super.selected 444 | overlay.hidden = !super.selected 445 | layer.borderWidth = super.selected ? 0.6 : 0 446 | } 447 | } 448 | 449 | override init(frame: CGRect) { 450 | super.init(frame: frame) 451 | 452 | imageView.frame = self.bounds 453 | overlay.frame = self.bounds 454 | overlay.backgroundColor = UIColor.whiteColor().colorWithAlphaComponent(0.2) 455 | self.contentView.addSubview(imageView) 456 | self.contentView.addSubview(overlay) 457 | self.contentView.addSubview(selectedView) 458 | } 459 | 460 | required init(coder aDecoder: NSCoder) { 461 | fatalError("init(coder:) has not been implemented") 462 | } 463 | 464 | override func layoutSubviews() { 465 | super.layoutSubviews() 466 | 467 | imageView.frame = self.bounds 468 | overlay.frame = self.bounds 469 | selectedView.frame.size = CGSizeMake(self.contentView.bounds.width / 4, self.contentView.bounds.height / 4) 470 | selectedView.frame.origin = CGPoint(x: self.contentView.bounds.width - selectedView.bounds.width - 5, 471 | y: self.contentView.bounds.height - selectedView.bounds.height - 5) 472 | } 473 | } 474 | 475 | // MARK: - AATakePhotoCollectionCell 476 | class AATakePhotoCollectionCell: UICollectionViewCell { 477 | private var imageView = UIImageView(image: UIImage(named: "take_photo")) 478 | 479 | override init(frame: CGRect) { 480 | super.init(frame: frame) 481 | 482 | imageView.frame = self.bounds 483 | self.contentView.addSubview(imageView) 484 | } 485 | 486 | required init(coder aDecoder: NSCoder) { 487 | fatalError("init(coder:) has not been implemented") 488 | } 489 | 490 | override func layoutSubviews() { 491 | super.layoutSubviews() 492 | 493 | imageView.frame = self.bounds 494 | } 495 | } 496 | 497 | -------------------------------------------------------------------------------- /AAImagePickerControllerDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 8A678A861B077A480036A1BC /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A678A851B077A480036A1BC /* AppDelegate.swift */; }; 11 | 8A678A881B077A480036A1BC /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A678A871B077A480036A1BC /* ViewController.swift */; }; 12 | 8A678A8B1B077A480036A1BC /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 8A678A891B077A480036A1BC /* Main.storyboard */; }; 13 | 8A678A8D1B077A480036A1BC /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8A678A8C1B077A480036A1BC /* Images.xcassets */; }; 14 | 8A678A901B077A480036A1BC /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8A678A8E1B077A480036A1BC /* LaunchScreen.xib */; }; 15 | 8A678A9C1B077A490036A1BC /* AAImagePickerControllerDemoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A678A9B1B077A490036A1BC /* AAImagePickerControllerDemoTests.swift */; }; 16 | 8A678AAB1B077AB60036A1BC /* AAImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A678AAA1B077AB60036A1BC /* AAImagePickerController.swift */; }; 17 | 8A678AAD1B078CF60036A1BC /* AKPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A678AAC1B078CF60036A1BC /* AKPickerView.swift */; }; 18 | 8A678AAF1B07C4C10036A1BC /* take_photo.png in Resources */ = {isa = PBXBuildFile; fileRef = 8A678AAE1B07C4C10036A1BC /* take_photo.png */; }; 19 | 8A678AB11B0811C50036A1BC /* check.png in Resources */ = {isa = PBXBuildFile; fileRef = 8A678AB01B0811C50036A1BC /* check.png */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXContainerItemProxy section */ 23 | 8A678A961B077A490036A1BC /* PBXContainerItemProxy */ = { 24 | isa = PBXContainerItemProxy; 25 | containerPortal = 8A678A781B077A480036A1BC /* Project object */; 26 | proxyType = 1; 27 | remoteGlobalIDString = 8A678A7F1B077A480036A1BC; 28 | remoteInfo = AAImagePickerControllerDemo; 29 | }; 30 | /* End PBXContainerItemProxy section */ 31 | 32 | /* Begin PBXFileReference section */ 33 | 8A678A801B077A480036A1BC /* AAImagePickerControllerDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AAImagePickerControllerDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 34 | 8A678A841B077A480036A1BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 35 | 8A678A851B077A480036A1BC /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 36 | 8A678A871B077A480036A1BC /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 37 | 8A678A8A1B077A480036A1BC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 38 | 8A678A8C1B077A480036A1BC /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; 39 | 8A678A8F1B077A480036A1BC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 40 | 8A678A951B077A490036A1BC /* AAImagePickerControllerDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AAImagePickerControllerDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 8A678A9A1B077A490036A1BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 42 | 8A678A9B1B077A490036A1BC /* AAImagePickerControllerDemoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AAImagePickerControllerDemoTests.swift; sourceTree = ""; }; 43 | 8A678AAA1B077AB60036A1BC /* AAImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AAImagePickerController.swift; sourceTree = ""; }; 44 | 8A678AAC1B078CF60036A1BC /* AKPickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AKPickerView.swift; sourceTree = ""; }; 45 | 8A678AAE1B07C4C10036A1BC /* take_photo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = take_photo.png; sourceTree = ""; }; 46 | 8A678AB01B0811C50036A1BC /* check.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = check.png; sourceTree = ""; }; 47 | /* End PBXFileReference section */ 48 | 49 | /* Begin PBXFrameworksBuildPhase section */ 50 | 8A678A7D1B077A480036A1BC /* Frameworks */ = { 51 | isa = PBXFrameworksBuildPhase; 52 | buildActionMask = 2147483647; 53 | files = ( 54 | ); 55 | runOnlyForDeploymentPostprocessing = 0; 56 | }; 57 | 8A678A921B077A490036A1BC /* Frameworks */ = { 58 | isa = PBXFrameworksBuildPhase; 59 | buildActionMask = 2147483647; 60 | files = ( 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | 8A678A771B077A480036A1BC = { 68 | isa = PBXGroup; 69 | children = ( 70 | 8A678AAC1B078CF60036A1BC /* AKPickerView.swift */, 71 | 8A678AA91B077AB60036A1BC /* AAImagePickerController */, 72 | 8A678A821B077A480036A1BC /* AAImagePickerControllerDemo */, 73 | 8A678A981B077A490036A1BC /* AAImagePickerControllerDemoTests */, 74 | 8A678A811B077A480036A1BC /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 8A678A811B077A480036A1BC /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 8A678A801B077A480036A1BC /* AAImagePickerControllerDemo.app */, 82 | 8A678A951B077A490036A1BC /* AAImagePickerControllerDemoTests.xctest */, 83 | ); 84 | name = Products; 85 | sourceTree = ""; 86 | }; 87 | 8A678A821B077A480036A1BC /* AAImagePickerControllerDemo */ = { 88 | isa = PBXGroup; 89 | children = ( 90 | 8A678A851B077A480036A1BC /* AppDelegate.swift */, 91 | 8A678A871B077A480036A1BC /* ViewController.swift */, 92 | 8A678A891B077A480036A1BC /* Main.storyboard */, 93 | 8A678A8C1B077A480036A1BC /* Images.xcassets */, 94 | 8A678A8E1B077A480036A1BC /* LaunchScreen.xib */, 95 | 8A678A831B077A480036A1BC /* Supporting Files */, 96 | ); 97 | path = AAImagePickerControllerDemo; 98 | sourceTree = ""; 99 | }; 100 | 8A678A831B077A480036A1BC /* Supporting Files */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 8A678A841B077A480036A1BC /* Info.plist */, 104 | ); 105 | name = "Supporting Files"; 106 | sourceTree = ""; 107 | }; 108 | 8A678A981B077A490036A1BC /* AAImagePickerControllerDemoTests */ = { 109 | isa = PBXGroup; 110 | children = ( 111 | 8A678A9B1B077A490036A1BC /* AAImagePickerControllerDemoTests.swift */, 112 | 8A678A991B077A490036A1BC /* Supporting Files */, 113 | ); 114 | path = AAImagePickerControllerDemoTests; 115 | sourceTree = ""; 116 | }; 117 | 8A678A991B077A490036A1BC /* Supporting Files */ = { 118 | isa = PBXGroup; 119 | children = ( 120 | 8A678A9A1B077A490036A1BC /* Info.plist */, 121 | ); 122 | name = "Supporting Files"; 123 | sourceTree = ""; 124 | }; 125 | 8A678AA91B077AB60036A1BC /* AAImagePickerController */ = { 126 | isa = PBXGroup; 127 | children = ( 128 | 8A678AB01B0811C50036A1BC /* check.png */, 129 | 8A678AAE1B07C4C10036A1BC /* take_photo.png */, 130 | 8A678AAA1B077AB60036A1BC /* AAImagePickerController.swift */, 131 | ); 132 | path = AAImagePickerController; 133 | sourceTree = ""; 134 | }; 135 | /* End PBXGroup section */ 136 | 137 | /* Begin PBXNativeTarget section */ 138 | 8A678A7F1B077A480036A1BC /* AAImagePickerControllerDemo */ = { 139 | isa = PBXNativeTarget; 140 | buildConfigurationList = 8A678A9F1B077A490036A1BC /* Build configuration list for PBXNativeTarget "AAImagePickerControllerDemo" */; 141 | buildPhases = ( 142 | 8A678A7C1B077A480036A1BC /* Sources */, 143 | 8A678A7D1B077A480036A1BC /* Frameworks */, 144 | 8A678A7E1B077A480036A1BC /* Resources */, 145 | ); 146 | buildRules = ( 147 | ); 148 | dependencies = ( 149 | ); 150 | name = AAImagePickerControllerDemo; 151 | productName = AAImagePickerControllerDemo; 152 | productReference = 8A678A801B077A480036A1BC /* AAImagePickerControllerDemo.app */; 153 | productType = "com.apple.product-type.application"; 154 | }; 155 | 8A678A941B077A490036A1BC /* AAImagePickerControllerDemoTests */ = { 156 | isa = PBXNativeTarget; 157 | buildConfigurationList = 8A678AA21B077A490036A1BC /* Build configuration list for PBXNativeTarget "AAImagePickerControllerDemoTests" */; 158 | buildPhases = ( 159 | 8A678A911B077A490036A1BC /* Sources */, 160 | 8A678A921B077A490036A1BC /* Frameworks */, 161 | 8A678A931B077A490036A1BC /* Resources */, 162 | ); 163 | buildRules = ( 164 | ); 165 | dependencies = ( 166 | 8A678A971B077A490036A1BC /* PBXTargetDependency */, 167 | ); 168 | name = AAImagePickerControllerDemoTests; 169 | productName = AAImagePickerControllerDemoTests; 170 | productReference = 8A678A951B077A490036A1BC /* AAImagePickerControllerDemoTests.xctest */; 171 | productType = "com.apple.product-type.bundle.unit-test"; 172 | }; 173 | /* End PBXNativeTarget section */ 174 | 175 | /* Begin PBXProject section */ 176 | 8A678A781B077A480036A1BC /* Project object */ = { 177 | isa = PBXProject; 178 | attributes = { 179 | LastUpgradeCheck = 0630; 180 | ORGANIZATIONNAME = Anas; 181 | TargetAttributes = { 182 | 8A678A7F1B077A480036A1BC = { 183 | CreatedOnToolsVersion = 6.3; 184 | }; 185 | 8A678A941B077A490036A1BC = { 186 | CreatedOnToolsVersion = 6.3; 187 | TestTargetID = 8A678A7F1B077A480036A1BC; 188 | }; 189 | }; 190 | }; 191 | buildConfigurationList = 8A678A7B1B077A480036A1BC /* Build configuration list for PBXProject "AAImagePickerControllerDemo" */; 192 | compatibilityVersion = "Xcode 3.2"; 193 | developmentRegion = English; 194 | hasScannedForEncodings = 0; 195 | knownRegions = ( 196 | en, 197 | Base, 198 | ); 199 | mainGroup = 8A678A771B077A480036A1BC; 200 | productRefGroup = 8A678A811B077A480036A1BC /* Products */; 201 | projectDirPath = ""; 202 | projectRoot = ""; 203 | targets = ( 204 | 8A678A7F1B077A480036A1BC /* AAImagePickerControllerDemo */, 205 | 8A678A941B077A490036A1BC /* AAImagePickerControllerDemoTests */, 206 | ); 207 | }; 208 | /* End PBXProject section */ 209 | 210 | /* Begin PBXResourcesBuildPhase section */ 211 | 8A678A7E1B077A480036A1BC /* Resources */ = { 212 | isa = PBXResourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | 8A678A8B1B077A480036A1BC /* Main.storyboard in Resources */, 216 | 8A678AB11B0811C50036A1BC /* check.png in Resources */, 217 | 8A678A901B077A480036A1BC /* LaunchScreen.xib in Resources */, 218 | 8A678A8D1B077A480036A1BC /* Images.xcassets in Resources */, 219 | 8A678AAF1B07C4C10036A1BC /* take_photo.png in Resources */, 220 | ); 221 | runOnlyForDeploymentPostprocessing = 0; 222 | }; 223 | 8A678A931B077A490036A1BC /* Resources */ = { 224 | isa = PBXResourcesBuildPhase; 225 | buildActionMask = 2147483647; 226 | files = ( 227 | ); 228 | runOnlyForDeploymentPostprocessing = 0; 229 | }; 230 | /* End PBXResourcesBuildPhase section */ 231 | 232 | /* Begin PBXSourcesBuildPhase section */ 233 | 8A678A7C1B077A480036A1BC /* Sources */ = { 234 | isa = PBXSourcesBuildPhase; 235 | buildActionMask = 2147483647; 236 | files = ( 237 | 8A678A881B077A480036A1BC /* ViewController.swift in Sources */, 238 | 8A678AAB1B077AB60036A1BC /* AAImagePickerController.swift in Sources */, 239 | 8A678AAD1B078CF60036A1BC /* AKPickerView.swift in Sources */, 240 | 8A678A861B077A480036A1BC /* AppDelegate.swift in Sources */, 241 | ); 242 | runOnlyForDeploymentPostprocessing = 0; 243 | }; 244 | 8A678A911B077A490036A1BC /* Sources */ = { 245 | isa = PBXSourcesBuildPhase; 246 | buildActionMask = 2147483647; 247 | files = ( 248 | 8A678A9C1B077A490036A1BC /* AAImagePickerControllerDemoTests.swift in Sources */, 249 | ); 250 | runOnlyForDeploymentPostprocessing = 0; 251 | }; 252 | /* End PBXSourcesBuildPhase section */ 253 | 254 | /* Begin PBXTargetDependency section */ 255 | 8A678A971B077A490036A1BC /* PBXTargetDependency */ = { 256 | isa = PBXTargetDependency; 257 | target = 8A678A7F1B077A480036A1BC /* AAImagePickerControllerDemo */; 258 | targetProxy = 8A678A961B077A490036A1BC /* PBXContainerItemProxy */; 259 | }; 260 | /* End PBXTargetDependency section */ 261 | 262 | /* Begin PBXVariantGroup section */ 263 | 8A678A891B077A480036A1BC /* Main.storyboard */ = { 264 | isa = PBXVariantGroup; 265 | children = ( 266 | 8A678A8A1B077A480036A1BC /* Base */, 267 | ); 268 | name = Main.storyboard; 269 | sourceTree = ""; 270 | }; 271 | 8A678A8E1B077A480036A1BC /* LaunchScreen.xib */ = { 272 | isa = PBXVariantGroup; 273 | children = ( 274 | 8A678A8F1B077A480036A1BC /* Base */, 275 | ); 276 | name = LaunchScreen.xib; 277 | sourceTree = ""; 278 | }; 279 | /* End PBXVariantGroup section */ 280 | 281 | /* Begin XCBuildConfiguration section */ 282 | 8A678A9D1B077A490036A1BC /* Debug */ = { 283 | isa = XCBuildConfiguration; 284 | buildSettings = { 285 | ALWAYS_SEARCH_USER_PATHS = NO; 286 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 287 | CLANG_CXX_LIBRARY = "libc++"; 288 | CLANG_ENABLE_MODULES = YES; 289 | CLANG_ENABLE_OBJC_ARC = YES; 290 | CLANG_WARN_BOOL_CONVERSION = YES; 291 | CLANG_WARN_CONSTANT_CONVERSION = YES; 292 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 293 | CLANG_WARN_EMPTY_BODY = YES; 294 | CLANG_WARN_ENUM_CONVERSION = YES; 295 | CLANG_WARN_INT_CONVERSION = YES; 296 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 297 | CLANG_WARN_UNREACHABLE_CODE = YES; 298 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 299 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 300 | COPY_PHASE_STRIP = NO; 301 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 302 | ENABLE_STRICT_OBJC_MSGSEND = YES; 303 | GCC_C_LANGUAGE_STANDARD = gnu99; 304 | GCC_DYNAMIC_NO_PIC = NO; 305 | GCC_NO_COMMON_BLOCKS = YES; 306 | GCC_OPTIMIZATION_LEVEL = 0; 307 | GCC_PREPROCESSOR_DEFINITIONS = ( 308 | "DEBUG=1", 309 | "$(inherited)", 310 | ); 311 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 312 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 313 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 314 | GCC_WARN_UNDECLARED_SELECTOR = YES; 315 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 316 | GCC_WARN_UNUSED_FUNCTION = YES; 317 | GCC_WARN_UNUSED_VARIABLE = YES; 318 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 319 | MTL_ENABLE_DEBUG_INFO = YES; 320 | ONLY_ACTIVE_ARCH = YES; 321 | SDKROOT = iphoneos; 322 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 323 | TARGETED_DEVICE_FAMILY = "1,2"; 324 | }; 325 | name = Debug; 326 | }; 327 | 8A678A9E1B077A490036A1BC /* Release */ = { 328 | isa = XCBuildConfiguration; 329 | buildSettings = { 330 | ALWAYS_SEARCH_USER_PATHS = NO; 331 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 332 | CLANG_CXX_LIBRARY = "libc++"; 333 | CLANG_ENABLE_MODULES = YES; 334 | CLANG_ENABLE_OBJC_ARC = YES; 335 | CLANG_WARN_BOOL_CONVERSION = YES; 336 | CLANG_WARN_CONSTANT_CONVERSION = YES; 337 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 338 | CLANG_WARN_EMPTY_BODY = YES; 339 | CLANG_WARN_ENUM_CONVERSION = YES; 340 | CLANG_WARN_INT_CONVERSION = YES; 341 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 342 | CLANG_WARN_UNREACHABLE_CODE = YES; 343 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 344 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 345 | COPY_PHASE_STRIP = NO; 346 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 347 | ENABLE_NS_ASSERTIONS = NO; 348 | ENABLE_STRICT_OBJC_MSGSEND = YES; 349 | GCC_C_LANGUAGE_STANDARD = gnu99; 350 | GCC_NO_COMMON_BLOCKS = YES; 351 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 352 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 353 | GCC_WARN_UNDECLARED_SELECTOR = YES; 354 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 355 | GCC_WARN_UNUSED_FUNCTION = YES; 356 | GCC_WARN_UNUSED_VARIABLE = YES; 357 | IPHONEOS_DEPLOYMENT_TARGET = 8.3; 358 | MTL_ENABLE_DEBUG_INFO = NO; 359 | SDKROOT = iphoneos; 360 | TARGETED_DEVICE_FAMILY = "1,2"; 361 | VALIDATE_PRODUCT = YES; 362 | }; 363 | name = Release; 364 | }; 365 | 8A678AA01B077A490036A1BC /* Debug */ = { 366 | isa = XCBuildConfiguration; 367 | buildSettings = { 368 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 369 | INFOPLIST_FILE = AAImagePickerControllerDemo/Info.plist; 370 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 371 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 372 | PRODUCT_NAME = "$(TARGET_NAME)"; 373 | }; 374 | name = Debug; 375 | }; 376 | 8A678AA11B077A490036A1BC /* Release */ = { 377 | isa = XCBuildConfiguration; 378 | buildSettings = { 379 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 380 | INFOPLIST_FILE = AAImagePickerControllerDemo/Info.plist; 381 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 382 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 383 | PRODUCT_NAME = "$(TARGET_NAME)"; 384 | }; 385 | name = Release; 386 | }; 387 | 8A678AA31B077A490036A1BC /* Debug */ = { 388 | isa = XCBuildConfiguration; 389 | buildSettings = { 390 | BUNDLE_LOADER = "$(TEST_HOST)"; 391 | FRAMEWORK_SEARCH_PATHS = ( 392 | "$(SDKROOT)/Developer/Library/Frameworks", 393 | "$(inherited)", 394 | ); 395 | GCC_PREPROCESSOR_DEFINITIONS = ( 396 | "DEBUG=1", 397 | "$(inherited)", 398 | ); 399 | INFOPLIST_FILE = AAImagePickerControllerDemoTests/Info.plist; 400 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 401 | PRODUCT_NAME = "$(TARGET_NAME)"; 402 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AAImagePickerControllerDemo.app/AAImagePickerControllerDemo"; 403 | }; 404 | name = Debug; 405 | }; 406 | 8A678AA41B077A490036A1BC /* Release */ = { 407 | isa = XCBuildConfiguration; 408 | buildSettings = { 409 | BUNDLE_LOADER = "$(TEST_HOST)"; 410 | FRAMEWORK_SEARCH_PATHS = ( 411 | "$(SDKROOT)/Developer/Library/Frameworks", 412 | "$(inherited)", 413 | ); 414 | INFOPLIST_FILE = AAImagePickerControllerDemoTests/Info.plist; 415 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 416 | PRODUCT_NAME = "$(TARGET_NAME)"; 417 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AAImagePickerControllerDemo.app/AAImagePickerControllerDemo"; 418 | }; 419 | name = Release; 420 | }; 421 | /* End XCBuildConfiguration section */ 422 | 423 | /* Begin XCConfigurationList section */ 424 | 8A678A7B1B077A480036A1BC /* Build configuration list for PBXProject "AAImagePickerControllerDemo" */ = { 425 | isa = XCConfigurationList; 426 | buildConfigurations = ( 427 | 8A678A9D1B077A490036A1BC /* Debug */, 428 | 8A678A9E1B077A490036A1BC /* Release */, 429 | ); 430 | defaultConfigurationIsVisible = 0; 431 | defaultConfigurationName = Release; 432 | }; 433 | 8A678A9F1B077A490036A1BC /* Build configuration list for PBXNativeTarget "AAImagePickerControllerDemo" */ = { 434 | isa = XCConfigurationList; 435 | buildConfigurations = ( 436 | 8A678AA01B077A490036A1BC /* Debug */, 437 | 8A678AA11B077A490036A1BC /* Release */, 438 | ); 439 | defaultConfigurationIsVisible = 0; 440 | }; 441 | 8A678AA21B077A490036A1BC /* Build configuration list for PBXNativeTarget "AAImagePickerControllerDemoTests" */ = { 442 | isa = XCConfigurationList; 443 | buildConfigurations = ( 444 | 8A678AA31B077A490036A1BC /* Debug */, 445 | 8A678AA41B077A490036A1BC /* Release */, 446 | ); 447 | defaultConfigurationIsVisible = 0; 448 | }; 449 | /* End XCConfigurationList section */ 450 | }; 451 | rootObject = 8A678A781B077A480036A1BC /* Project object */; 452 | } 453 | -------------------------------------------------------------------------------- /AKPickerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AKPickerView.swift 3 | // AKPickerView 4 | // 5 | // Created by Akio Yasui on 1/29/15. 6 | // Copyright (c) 2015 Akkyie Y. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | /** 12 | Styles of AKPickerView. 13 | 14 | - Wheel: Style with 3D appearance like UIPickerView. 15 | - Flat: Flat style. 16 | */ 17 | public enum AKPickerViewStyle { 18 | case Wheel 19 | case Flat 20 | } 21 | 22 | // MARK: - Protocols 23 | // MARK: AKPickerViewDataSource 24 | /** 25 | Protocols to specify the number and type of contents. 26 | */ 27 | @objc public protocol AKPickerViewDataSource { 28 | func numberOfItemsInPickerView(pickerView: AKPickerView) -> Int 29 | optional func pickerView(pickerView: AKPickerView, titleForItem item: Int) -> String 30 | optional func pickerView(pickerView: AKPickerView, imageForItem item: Int) -> UIImage 31 | } 32 | 33 | // MARK: AKPickerViewDelegate 34 | /** 35 | Protocols to specify the attitude when user selected an item, 36 | and customize the appearance of labels. 37 | */ 38 | @objc public protocol AKPickerViewDelegate: UIScrollViewDelegate { 39 | optional func pickerView(pickerView: AKPickerView, didSelectItem item: Int) 40 | optional func pickerView(pickerView: AKPickerView, marginForItem item: Int) -> CGSize 41 | optional func pickerView(pickerView: AKPickerView, configureLabel label: UILabel, forItem item: Int) 42 | } 43 | 44 | // MARK: - Private Classes and Protocols 45 | // MARK: AKCollectionViewLayoutDelegate 46 | /** 47 | Private. Used to deliver the style of the picker. 48 | */ 49 | private protocol AKCollectionViewLayoutDelegate { 50 | func pickerViewStyleForCollectionViewLayout(layout: AKCollectionViewLayout) -> AKPickerViewStyle 51 | } 52 | 53 | // MARK: AKCollectionViewCell 54 | /** 55 | Private. A subclass of UICollectionViewCell used in AKPickerView's collection view. 56 | */ 57 | private class AKCollectionViewCell: UICollectionViewCell { 58 | var label: UILabel! 59 | var imageView: UIImageView! 60 | var font = UIFont.systemFontOfSize(UIFont.systemFontSize()) 61 | var highlightedFont = UIFont.systemFontOfSize(UIFont.systemFontSize()) 62 | var _selected: Bool = false { 63 | didSet(selected) { 64 | let animation = CATransition() 65 | animation.type = kCATransitionFade 66 | animation.duration = 0.15 67 | self.label.layer.addAnimation(animation, forKey: "") 68 | self.label.font = self.selected ? self.highlightedFont : self.font 69 | } 70 | } 71 | 72 | func initialize() { 73 | self.layer.doubleSided = false 74 | self.layer.shouldRasterize = true 75 | self.layer.rasterizationScale = UIScreen.mainScreen().scale 76 | 77 | self.label = UILabel(frame: self.contentView.bounds) 78 | self.label.backgroundColor = UIColor.clearColor() 79 | self.label.textAlignment = .Center 80 | self.label.textColor = UIColor.grayColor() 81 | self.label.numberOfLines = 1 82 | self.label.lineBreakMode = .ByTruncatingTail 83 | self.label.highlightedTextColor = UIColor.blackColor() 84 | self.label.font = self.font 85 | self.label.autoresizingMask = 86 | .FlexibleTopMargin | 87 | .FlexibleLeftMargin | 88 | .FlexibleBottomMargin | 89 | .FlexibleRightMargin; 90 | self.contentView.addSubview(self.label) 91 | 92 | self.imageView = UIImageView(frame: self.contentView.bounds) 93 | self.imageView.backgroundColor = UIColor.clearColor() 94 | self.imageView.contentMode = .Center 95 | self.imageView.autoresizingMask = .FlexibleWidth | .FlexibleHeight; 96 | self.contentView.addSubview(self.imageView) 97 | } 98 | 99 | init() { 100 | super.init(frame: CGRectZero) 101 | self.initialize() 102 | } 103 | 104 | override init(frame: CGRect) { 105 | super.init(frame: frame) 106 | self.initialize() 107 | } 108 | 109 | required init(coder aDecoder: NSCoder) { 110 | super.init(coder: aDecoder) 111 | self.initialize() 112 | } 113 | } 114 | 115 | // MARK: AKCollectionViewLayout 116 | /** 117 | Private. A subclass of UICollectionViewFlowLayout used in the collection view. 118 | */ 119 | private class AKCollectionViewLayout: UICollectionViewFlowLayout { 120 | var delegate: AKCollectionViewLayoutDelegate! 121 | var width: CGFloat! 122 | var midX: CGFloat! 123 | var maxAngle: CGFloat! 124 | 125 | func initialize() { 126 | self.sectionInset = UIEdgeInsetsMake(0.0, 0.0, 0.0, 0.0) 127 | self.scrollDirection = .Horizontal 128 | self.minimumLineSpacing = 0.0 129 | } 130 | 131 | override init() { 132 | super.init() 133 | self.initialize() 134 | } 135 | 136 | required init(coder aDecoder: NSCoder) { 137 | super.init(coder: aDecoder) 138 | self.initialize() 139 | } 140 | 141 | private override func prepareLayout() { 142 | let visibleRect = CGRect(origin: self.collectionView!.contentOffset, size: self.collectionView!.bounds.size) 143 | self.midX = CGRectGetMidX(visibleRect); 144 | self.width = CGRectGetWidth(visibleRect) / 2; 145 | self.maxAngle = CGFloat(M_PI_2); 146 | } 147 | 148 | private override func shouldInvalidateLayoutForBoundsChange(newBounds: CGRect) -> Bool { 149 | return true 150 | } 151 | 152 | private override func layoutAttributesForItemAtIndexPath(indexPath: NSIndexPath) -> UICollectionViewLayoutAttributes! { 153 | let attributes = super.layoutAttributesForItemAtIndexPath(indexPath) 154 | switch self.delegate.pickerViewStyleForCollectionViewLayout(self) { 155 | case .Flat: 156 | return attributes 157 | case .Wheel: 158 | let distance = CGRectGetMidX(attributes.frame) - self.midX; 159 | let currentAngle = self.maxAngle * distance / self.width / CGFloat(M_PI_2); 160 | var transform = CATransform3DIdentity; 161 | transform = CATransform3DTranslate(transform, -distance, 0, -self.width); 162 | transform = CATransform3DRotate(transform, currentAngle, 0, 1, 0); 163 | transform = CATransform3DTranslate(transform, 0, 0, self.width); 164 | attributes.transform3D = transform; 165 | attributes.alpha = fabs(currentAngle) < self.maxAngle ? 1.0 : 0.0; 166 | return attributes; 167 | } 168 | } 169 | 170 | private override func layoutAttributesForElementsInRect(rect: CGRect) -> [AnyObject]? { 171 | switch self.delegate.pickerViewStyleForCollectionViewLayout(self) { 172 | case .Flat: 173 | return super.layoutAttributesForElementsInRect(rect) 174 | case .Wheel: 175 | var attributes = [AnyObject]() 176 | if self.collectionView!.numberOfSections() > 0 { 177 | for i in 0 ..< self.collectionView!.numberOfItemsInSection(0) { 178 | let indexPath = NSIndexPath(forItem: i, inSection: 0) 179 | attributes.append(self.layoutAttributesForItemAtIndexPath(indexPath)) 180 | } 181 | } 182 | return attributes 183 | } 184 | } 185 | 186 | } 187 | 188 | // MARK: AKPickerViewDelegateIntercepter 189 | /** 190 | Private. Used to hook UICollectionViewDelegate and throw it AKPickerView, 191 | and if it conforms to UIScrollViewDelegate, also throw it to AKPickerView's delegate. 192 | */ 193 | private class AKPickerViewDelegateIntercepter: NSObject, UICollectionViewDelegate { 194 | weak var pickerView: AKPickerView? 195 | weak var delegate: UIScrollViewDelegate? 196 | 197 | init(pickerView: AKPickerView, delegate: UIScrollViewDelegate?) { 198 | self.pickerView = pickerView 199 | self.delegate = delegate 200 | } 201 | 202 | private override func forwardingTargetForSelector(aSelector: Selector) -> AnyObject? { 203 | if self.pickerView!.respondsToSelector(aSelector) { 204 | return self.pickerView 205 | } else if self.delegate != nil && self.delegate!.respondsToSelector(aSelector) { 206 | return self.delegate 207 | } else { 208 | return nil 209 | } 210 | } 211 | 212 | private override func respondsToSelector(aSelector: Selector) -> Bool { 213 | if self.pickerView!.respondsToSelector(aSelector) { 214 | return true 215 | } else if self.delegate != nil && self.delegate!.respondsToSelector(aSelector) { 216 | return true 217 | } else { 218 | return super.respondsToSelector(aSelector) 219 | } 220 | } 221 | 222 | } 223 | 224 | // MARK: - AKPickerView 225 | // TODO: Make these delegate conformation private 226 | /** 227 | Horizontal picker view. This is just a subclass of UIView, contains a UICollectionView. 228 | */ 229 | public class AKPickerView: UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, AKCollectionViewLayoutDelegate { 230 | 231 | // MARK: - Properties 232 | // MARK: Readwrite Properties 233 | /// Readwrite. Data source of picker view. 234 | public weak var dataSource: AKPickerViewDataSource? = nil 235 | /// Readwrite. Delegate of picker view. 236 | public weak var delegate: AKPickerViewDelegate? = nil { 237 | didSet(delegate) { 238 | self.intercepter.delegate = delegate 239 | } 240 | } 241 | /// Readwrite. A font which used in NOT selected cells. 242 | public lazy var font = UIFont.systemFontOfSize(20) 243 | /// Readwrite. A font which used in selected cells. 244 | public lazy var highlightedFont = UIFont.boldSystemFontOfSize(20) 245 | /// Readwrite. A color of the text on NOT selected cells. 246 | public lazy var textColor = UIColor.darkGrayColor() 247 | /// Readwrite. A color of the text on selected cells. 248 | public lazy var highlightedTextColor = UIColor.blackColor() 249 | /// Readwrite. A float value which indicates the spacing between cells. 250 | public var interitemSpacing: CGFloat = 0.0 251 | /// Readwrite. The style of the picker view. See AKPickerViewStyle. 252 | public var pickerViewStyle = AKPickerViewStyle.Wheel 253 | /// Readwrite. A float value which determines the perspective representation which used when using AKPickerViewStyle.Wheel style. 254 | public var viewDepth: CGFloat = 1000.0 { 255 | didSet { 256 | self.collectionView.layer.sublayerTransform = self.viewDepth > 0.0 ? { 257 | var transform = CATransform3DIdentity; 258 | transform.m34 = -1.0 / self.viewDepth; 259 | return transform; 260 | }() : CATransform3DIdentity; 261 | } 262 | } 263 | /// Readwrite. A boolean value indicates whether the mask is disabled. 264 | public var maskDisabled: Bool! = nil { 265 | didSet { 266 | self.collectionView.layer.mask = self.maskDisabled == true ? nil : { 267 | let maskLayer = CAGradientLayer() 268 | maskLayer.frame = self.collectionView.bounds 269 | maskLayer.colors = [ 270 | UIColor.clearColor().CGColor, 271 | UIColor.blackColor().CGColor, 272 | UIColor.blackColor().CGColor, 273 | UIColor.clearColor().CGColor] 274 | maskLayer.locations = [0.0, 0.33, 0.66, 1.0] 275 | maskLayer.startPoint = CGPointMake(0.0, 0.0) 276 | maskLayer.endPoint = CGPointMake(1.0, 0.0) 277 | return maskLayer 278 | }() 279 | } 280 | } 281 | 282 | // MARK: Readonly Properties 283 | /// Readonly. Index of currently selected item. 284 | private(set) var selectedItem: Int = 0 285 | /// Readonly. The point at which the origin of the content view is offset from the origin of the picker view. 286 | public var contentOffset: CGPoint { 287 | get { 288 | return self.collectionView.contentOffset 289 | } 290 | } 291 | 292 | // MARK: Private Properties 293 | /// Private. A UICollectionView which shows contents on cells. 294 | private var collectionView: UICollectionView! 295 | /// Private. An intercepter to hook UICollectionViewDelegate then throw it picker view and its delegate 296 | private var intercepter: AKPickerViewDelegateIntercepter! 297 | /// Private. A UICollectionViewFlowLayout used in picker view's collection view. 298 | private var collectionViewLayout: AKCollectionViewLayout { 299 | let layout = AKCollectionViewLayout() 300 | layout.delegate = self 301 | return layout 302 | } 303 | 304 | // MARK: - Functions 305 | // MARK: View Lifecycle 306 | /** 307 | Private. Initializes picker view's subviews and friends. 308 | */ 309 | private func initialize() { 310 | self.collectionView?.removeFromSuperview() 311 | self.collectionView = UICollectionView(frame: self.bounds, collectionViewLayout: self.collectionViewLayout) 312 | self.collectionView.showsHorizontalScrollIndicator = false 313 | self.collectionView.backgroundColor = UIColor.clearColor() 314 | self.collectionView.decelerationRate = UIScrollViewDecelerationRateFast 315 | self.collectionView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight 316 | self.collectionView.dataSource = self 317 | self.collectionView.registerClass( 318 | AKCollectionViewCell.self, 319 | forCellWithReuseIdentifier: NSStringFromClass(AKCollectionViewCell.self)) 320 | self.addSubview(self.collectionView) 321 | 322 | self.intercepter = AKPickerViewDelegateIntercepter(pickerView: self, delegate: self.delegate) 323 | self.collectionView.delegate = self.intercepter 324 | 325 | self.maskDisabled = self.maskDisabled == nil ? false : self.maskDisabled 326 | } 327 | 328 | public init() { 329 | super.init(frame: CGRectZero) 330 | self.initialize() 331 | } 332 | 333 | public override init(frame: CGRect) { 334 | super.init(frame: frame) 335 | self.initialize() 336 | } 337 | 338 | public required init(coder aDecoder: NSCoder) { 339 | super.init(coder: aDecoder) 340 | self.initialize() 341 | } 342 | 343 | deinit { 344 | self.collectionView.delegate = nil 345 | } 346 | 347 | // MARK: Layout 348 | 349 | public override func layoutSubviews() { 350 | super.layoutSubviews() 351 | if self.dataSource != nil && self.dataSource!.numberOfItemsInPickerView(self) > 0 { 352 | self.collectionView.collectionViewLayout = self.collectionViewLayout 353 | self.scrollToItem(self.selectedItem, animated: false) 354 | } 355 | self.collectionView.layer.mask?.frame = self.collectionView.bounds 356 | } 357 | 358 | public override func intrinsicContentSize() -> CGSize { 359 | return CGSizeMake(UIViewNoIntrinsicMetric, max(self.font.lineHeight, self.highlightedFont.lineHeight)) 360 | } 361 | 362 | // MARK: Calculation Functions 363 | 364 | /** 365 | Private. Used to calculate bounding size of given string with picker view's font and highlightedFont 366 | 367 | :param: string A NSString to calculate size 368 | :returns: A CGSize which contains given string just. 369 | */ 370 | private func sizeForString(string: NSString) -> CGSize { 371 | let size = string.sizeWithAttributes([NSFontAttributeName: self.font]) 372 | let highlightedSize = string.sizeWithAttributes([NSFontAttributeName: self.highlightedFont]) 373 | return CGSize( 374 | width: ceil(max(size.width, highlightedSize.width)), 375 | height: ceil(max(size.height, highlightedSize.height))) 376 | } 377 | 378 | /** 379 | Private. Used to calculate the x-coordinate of the content offset of specified item. 380 | 381 | :param: item An integer value which indicates the index of cell. 382 | :returns: An x-coordinate of the cell whose index is given one. 383 | */ 384 | private func offsetForItem(item: Int) -> CGFloat { 385 | var offset: CGFloat = 0 386 | for i in 0 ..< item { 387 | let indexPath = NSIndexPath(forItem: i, inSection: 0) 388 | let cellSize = self.collectionView( 389 | self.collectionView, 390 | layout: self.collectionView.collectionViewLayout, 391 | sizeForItemAtIndexPath: indexPath) 392 | offset += cellSize.width 393 | } 394 | 395 | let firstIndexPath = NSIndexPath(forItem: 0, inSection: 0) 396 | let firstSize = self.collectionView( 397 | self.collectionView, 398 | layout: self.collectionView.collectionViewLayout, 399 | sizeForItemAtIndexPath: firstIndexPath) 400 | let selectedIndexPath = NSIndexPath(forItem: item, inSection: 0) 401 | let selectedSize = self.collectionView( 402 | self.collectionView, 403 | layout: self.collectionView.collectionViewLayout, 404 | sizeForItemAtIndexPath: selectedIndexPath) 405 | offset -= (firstSize.width - selectedSize.width) / 2.0 406 | 407 | return offset 408 | } 409 | 410 | // MARK: View Controls 411 | /** 412 | Reload the picker view's contents and styles. Call this method always after any property is changed. 413 | */ 414 | public func reloadData() { 415 | self.invalidateIntrinsicContentSize() 416 | self.collectionView.collectionViewLayout.invalidateLayout() 417 | self.collectionView.reloadData() 418 | if self.dataSource != nil && self.dataSource!.numberOfItemsInPickerView(self) > 0 { 419 | self.selectItem(self.selectedItem, animated: false, notifySelection: false) 420 | } 421 | } 422 | 423 | /** 424 | Move to the cell whose index is given one without selection change. 425 | 426 | :param: item An integer value which indicates the index of cell. 427 | :param: animated True if the scrolling should be animated, false if it should be immediate. 428 | */ 429 | public func scrollToItem(item: Int, animated: Bool = false) { 430 | switch self.pickerViewStyle { 431 | case .Flat: 432 | self.collectionView.scrollToItemAtIndexPath( 433 | NSIndexPath( 434 | forItem: item, 435 | inSection: 0), 436 | atScrollPosition: .CenteredHorizontally, 437 | animated: animated) 438 | case .Wheel: 439 | self.collectionView.setContentOffset( 440 | CGPoint( 441 | x: self.offsetForItem(item), 442 | y: self.collectionView.contentOffset.y), 443 | animated: animated) 444 | } 445 | } 446 | 447 | /** 448 | Select a cell whose index is given one and move to it. 449 | 450 | :param: item An integer value which indicates the index of cell. 451 | :param: animated True if the scrolling should be animated, false if it should be immediate. 452 | */ 453 | public func selectItem(item: Int, animated: Bool = false) { 454 | self.selectItem(item, animated: animated, notifySelection: true) 455 | } 456 | 457 | /** 458 | Private. Select a cell whose index is given one and move to it, with specifying whether it calls delegate method. 459 | 460 | :param: item An integer value which indicates the index of cell. 461 | :param: animated True if the scrolling should be animated, false if it should be immediate. 462 | :param: notifySelection True if the delegate method should be called, false if not. 463 | */ 464 | private func selectItem(item: Int, animated: Bool, notifySelection: Bool) { 465 | self.collectionView.selectItemAtIndexPath( 466 | NSIndexPath(forItem: item, inSection: 0), 467 | animated: animated, 468 | scrollPosition: .None) 469 | self.scrollToItem(item, animated: animated) 470 | self.selectedItem = item 471 | if notifySelection { 472 | self.delegate?.pickerView?(self, didSelectItem: item) 473 | } 474 | } 475 | 476 | // MARK: Delegate Handling 477 | /** 478 | Private. 479 | */ 480 | private func didEndScrolling() { 481 | switch self.pickerViewStyle { 482 | case .Flat: 483 | let center = self.convertPoint(self.collectionView.center, toView: self.collectionView) 484 | if let indexPath = self.collectionView.indexPathForItemAtPoint(center) { 485 | self.selectItem(indexPath.item, animated: true, notifySelection: true) 486 | } 487 | case .Wheel: 488 | if let numberOfItems = self.dataSource?.numberOfItemsInPickerView(self) { 489 | for i in 0 ..< numberOfItems { 490 | let indexPath = NSIndexPath(forItem: i, inSection: 0) 491 | let cellSize = self.collectionView( 492 | self.collectionView, 493 | layout: self.collectionView.collectionViewLayout, 494 | sizeForItemAtIndexPath: indexPath) 495 | if self.offsetForItem(i) + cellSize.width / 2 > self.collectionView.contentOffset.x { 496 | self.selectItem(i, animated: true, notifySelection: true) 497 | break 498 | } 499 | } 500 | } 501 | } 502 | } 503 | 504 | // MARK: UICollectionViewDataSource 505 | public func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int { 506 | return self.dataSource != nil && self.dataSource!.numberOfItemsInPickerView(self) > 0 ? 1 : 0 507 | } 508 | 509 | public func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { 510 | return self.dataSource != nil ? self.dataSource!.numberOfItemsInPickerView(self) : 0 511 | } 512 | 513 | public func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell { 514 | let cell = collectionView.dequeueReusableCellWithReuseIdentifier(NSStringFromClass(AKCollectionViewCell.self), forIndexPath: indexPath) as! AKCollectionViewCell 515 | if let title = self.dataSource?.pickerView?(self, titleForItem: indexPath.item) { 516 | cell.label.text = title 517 | cell.label.textColor = self.textColor 518 | cell.label.highlightedTextColor = self.highlightedTextColor 519 | cell.label.font = self.font 520 | cell.font = self.font 521 | cell.highlightedFont = self.highlightedFont 522 | cell.label.bounds = CGRect(origin: CGPointZero, size: self.sizeForString(title)) 523 | if let delegate = self.delegate { 524 | delegate.pickerView?(self, configureLabel: cell.label, forItem: indexPath.item) 525 | if let margin = delegate.pickerView?(self, marginForItem: indexPath.item) { 526 | cell.label.frame = CGRectInset(cell.label.frame, -margin.width, -margin.height) 527 | } 528 | } 529 | } else if let image = self.dataSource?.pickerView?(self, imageForItem: indexPath.item) { 530 | cell.imageView.image = image 531 | } 532 | cell._selected = (indexPath.item == self.selectedItem) 533 | return cell 534 | } 535 | 536 | // MARK: UICollectionViewDelegateFlowLayout 537 | public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAtIndexPath indexPath: NSIndexPath) -> CGSize { 538 | var size = CGSizeMake(self.interitemSpacing, collectionView.bounds.size.height) 539 | if let title = self.dataSource?.pickerView?(self, titleForItem: indexPath.item) { 540 | size.width += self.sizeForString(title).width 541 | if let margin = self.delegate?.pickerView?(self, marginForItem: indexPath.item) { 542 | size.width += margin.width * 2 543 | } 544 | } else if let image = self.dataSource?.pickerView?(self, imageForItem: indexPath.item) { 545 | size.width += image.size.width 546 | } 547 | return size 548 | } 549 | 550 | public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAtIndex section: Int) -> CGFloat { 551 | return 0.0 552 | } 553 | 554 | public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAtIndex section: Int) -> CGFloat { 555 | return 0.0 556 | } 557 | 558 | public func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets { 559 | let number = self.collectionView(collectionView, numberOfItemsInSection: section) 560 | let firstIndexPath = NSIndexPath(forItem: 0, inSection: section) 561 | let firstSize = self.collectionView(collectionView, layout: collectionView.collectionViewLayout, sizeForItemAtIndexPath: firstIndexPath) 562 | let lastIndexPath = NSIndexPath(forItem: number - 1, inSection: section) 563 | let lastSize = self.collectionView(collectionView, layout: collectionView.collectionViewLayout, sizeForItemAtIndexPath: lastIndexPath) 564 | return UIEdgeInsetsMake( 565 | 0, (collectionView.bounds.size.width - firstSize.width) / 2, 566 | 0, (collectionView.bounds.size.width - lastSize.width) / 2 567 | ) 568 | } 569 | 570 | // MARK: UICollectionViewDelegate 571 | public func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) { 572 | self.selectItem(indexPath.item, animated: true) 573 | } 574 | 575 | // MARK: UIScrollViewDelegate 576 | public func scrollViewDidEndDecelerating(scrollView: UIScrollView) { 577 | self.delegate?.scrollViewDidEndDecelerating?(scrollView) 578 | self.didEndScrolling() 579 | } 580 | 581 | public func scrollViewDidEndDragging(scrollView: UIScrollView, willDecelerate decelerate: Bool) { 582 | self.delegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate) 583 | if !decelerate { 584 | self.didEndScrolling() 585 | } 586 | } 587 | 588 | public func scrollViewDidScroll(scrollView: UIScrollView) { 589 | self.delegate?.scrollViewDidScroll?(scrollView) 590 | CATransaction.begin() 591 | CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions) 592 | self.collectionView.layer.mask?.frame = self.collectionView.bounds 593 | CATransaction.commit() 594 | } 595 | 596 | // MARK: AKCollectionViewLayoutDelegate 597 | private func pickerViewStyleForCollectionViewLayout(layout: AKCollectionViewLayout) -> AKPickerViewStyle { 598 | return self.pickerViewStyle 599 | } 600 | 601 | } 602 | --------------------------------------------------------------------------------