vendoo_v1.0/Pods/BSImagePicker/Pod/Classes/Controller/PhotosViewController.swift

568 lines
24 KiB
Swift

// The MIT License (MIT)
//
// Copyright (c) 2015 Joakim Gyllström
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import UIKit
import Photos
import BSGridCollectionViewLayout
final class PhotosViewController : UICollectionViewController {
var selectionClosure: ((asset: PHAsset) -> Void)?
var deselectionClosure: ((asset: PHAsset) -> Void)?
var cancelClosure: ((assets: [PHAsset]) -> Void)?
var finishClosure: ((assets: [PHAsset]) -> Void)?
var doneBarButton: UIBarButtonItem?
var cancelBarButton: UIBarButtonItem?
var albumTitleView: AlbumTitleView?
let expandAnimator = ZoomAnimator()
let shrinkAnimator = ZoomAnimator()
private var photosDataSource: PhotoCollectionViewDataSource?
private var albumsDataSource: AlbumTableViewDataSource
private let cameraDataSource: CameraCollectionViewDataSource
private var composedDataSource: ComposedCollectionViewDataSource?
private var defaultSelections: PHFetchResult?
let settings: BSImagePickerSettings
private var doneBarButtonTitle: String?
lazy var albumsViewController: AlbumsViewController = {
let storyboard = UIStoryboard(name: "Albums", bundle: BSImagePickerViewController.bundle)
let vc = storyboard.instantiateInitialViewController() as! AlbumsViewController
vc.tableView.dataSource = self.albumsDataSource
vc.tableView.delegate = self
return vc
}()
private lazy var previewViewContoller: PreviewViewController? = {
return PreviewViewController(nibName: nil, bundle: nil)
}()
required init(fetchResults: [PHFetchResult], defaultSelections: PHFetchResult? = nil, settings aSettings: BSImagePickerSettings) {
albumsDataSource = AlbumTableViewDataSource(fetchResults: fetchResults)
cameraDataSource = CameraCollectionViewDataSource(settings: aSettings, cameraAvailable: UIImagePickerController.isSourceTypeAvailable(.Camera))
self.defaultSelections = defaultSelections
settings = aSettings
super.init(collectionViewLayout: GridCollectionViewLayout())
PHPhotoLibrary.sharedPhotoLibrary().registerChangeObserver(self)
}
required init?(coder aDecoder: NSCoder) {
fatalError("b0rk: initWithCoder not implemented")
}
deinit {
PHPhotoLibrary.sharedPhotoLibrary().unregisterChangeObserver(self)
}
override func loadView() {
super.loadView()
// Setup collection view
collectionView?.backgroundColor = UIColor.whiteColor()
collectionView?.allowsMultipleSelection = true
// Set an empty title to get < back button
title = " "
// Set button actions and add them to navigation item
doneBarButton?.target = self
doneBarButton?.action = #selector(PhotosViewController.doneButtonPressed(_:))
cancelBarButton?.target = self
cancelBarButton?.action = #selector(PhotosViewController.cancelButtonPressed(_:))
albumTitleView?.albumButton?.addTarget(self, action: #selector(PhotosViewController.albumButtonPressed(_:)), forControlEvents: .TouchUpInside)
navigationItem.leftBarButtonItem = cancelBarButton
navigationItem.rightBarButtonItem = doneBarButton
navigationItem.titleView = albumTitleView
if let album = albumsDataSource.fetchResults.first?.firstObject as? PHAssetCollection {
initializePhotosDataSource(album, selections: defaultSelections)
updateAlbumTitle(album)
synchronizeCollectionView()
}
// Add long press recognizer
let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(PhotosViewController.collectionViewLongPressed(_:)))
longPressRecognizer.minimumPressDuration = 0.5
collectionView?.addGestureRecognizer(longPressRecognizer)
// Set navigation controller delegate
navigationController?.delegate = self
// Register cells
photosDataSource?.registerCellIdentifiersForCollectionView(collectionView)
cameraDataSource.registerCellIdentifiersForCollectionView(collectionView)
}
// MARK: Appear/Disappear
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
updateDoneButton()
}
// MARK: Button actions
func cancelButtonPressed(sender: UIBarButtonItem) {
guard let closure = cancelClosure, let photosDataSource = photosDataSource else {
dismissViewControllerAnimated(true, completion: nil)
return
}
dispatch_async(dispatch_get_global_queue(0, 0), { () -> Void in
closure(assets: photosDataSource.selections)
})
dismissViewControllerAnimated(true, completion: nil)
}
func doneButtonPressed(sender: UIBarButtonItem) {
guard let closure = finishClosure, let photosDataSource = photosDataSource else {
dismissViewControllerAnimated(true, completion: nil)
return
}
dispatch_async(dispatch_get_global_queue(0, 0), { () -> Void in
closure(assets: photosDataSource.selections)
})
dismissViewControllerAnimated(true, completion: nil)
}
func albumButtonPressed(sender: UIButton) {
guard let popVC = albumsViewController.popoverPresentationController else {
return
}
popVC.permittedArrowDirections = .Up
popVC.sourceView = sender
let senderRect = sender.convertRect(sender.frame, fromView: sender.superview)
let sourceRect = CGRect(x: senderRect.origin.x, y: senderRect.origin.y + (sender.frame.size.height / 2), width: senderRect.size.width, height: senderRect.size.height)
popVC.sourceRect = sourceRect
popVC.delegate = self
albumsViewController.tableView.reloadData()
presentViewController(albumsViewController, animated: true, completion: nil)
}
func collectionViewLongPressed(sender: UIGestureRecognizer) {
if sender.state == .Began {
// Disable recognizer while we are figuring out location and pushing preview
sender.enabled = false
collectionView?.userInteractionEnabled = false
// Calculate which index path long press came from
let location = sender.locationInView(collectionView)
let indexPath = collectionView?.indexPathForItemAtPoint(location)
if let vc = previewViewContoller, let indexPath = indexPath, let cell = collectionView?.cellForItemAtIndexPath(indexPath) as? PhotoCell, let asset = cell.asset {
// Setup fetch options to be synchronous
let options = PHImageRequestOptions()
options.synchronous = true
// Load image for preview
if let imageView = vc.imageView {
PHCachingImageManager.defaultManager().requestImageForAsset(asset, targetSize:imageView.frame.size, contentMode: .AspectFit, options: options) { (result, _) in
imageView.image = result
}
}
// Setup animation
expandAnimator.sourceImageView = cell.imageView
expandAnimator.destinationImageView = vc.imageView
shrinkAnimator.sourceImageView = vc.imageView
shrinkAnimator.destinationImageView = cell.imageView
navigationController?.pushViewController(vc, animated: true)
}
// Re-enable recognizer, after animation is done
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(expandAnimator.transitionDuration(nil) * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), { () -> Void in
sender.enabled = true
self.collectionView?.userInteractionEnabled = true
})
}
}
// MARK: Private helper methods
func updateDoneButton() {
// Find right button
if let subViews = navigationController?.navigationBar.subviews, let photosDataSource = photosDataSource {
for view in subViews {
if let btn = view as? UIButton where checkIfRightButtonItem(btn) {
// Store original title if we havn't got it
if doneBarButtonTitle == nil {
doneBarButtonTitle = btn.titleForState(.Normal)
}
// Update title
if let doneBarButtonTitle = doneBarButtonTitle {
// Special case if we have selected 1 image and that is
// the max number of allowed selections
if (photosDataSource.selections.count == 1 && self.settings.maxNumberOfSelections == 1) {
btn.bs_setTitleWithoutAnimation("\(doneBarButtonTitle)", forState: .Normal)
} else if photosDataSource.selections.count > 0 {
btn.bs_setTitleWithoutAnimation("\(doneBarButtonTitle) (\(photosDataSource.selections.count))", forState: .Normal)
} else {
btn.bs_setTitleWithoutAnimation(doneBarButtonTitle, forState: .Normal)
}
// Enabled?
doneBarButton?.enabled = photosDataSource.selections.count > 0
}
// Stop loop
break
}
}
}
}
// Check if a give UIButton is the right UIBarButtonItem in the navigation bar
// Somewhere along the road, our UIBarButtonItem gets transformed to an UINavigationButton
func checkIfRightButtonItem(btn: UIButton) -> Bool {
guard let rightButton = navigationItem.rightBarButtonItem else {
return false
}
// Store previous values
let wasRightEnabled = rightButton.enabled
let wasButtonEnabled = btn.enabled
// Set a known state for both buttons
rightButton.enabled = false
btn.enabled = false
// Change one and see if other also changes
rightButton.enabled = true
let isRightButton = btn.enabled
// Reset
rightButton.enabled = wasRightEnabled
btn.enabled = wasButtonEnabled
return isRightButton
}
func synchronizeSelectionInCollectionView(collectionView: UICollectionView) {
guard let photosDataSource = photosDataSource else {
return
}
// Get indexes of the selected assets
let mutableIndexSet = NSMutableIndexSet()
for object in photosDataSource.selections {
let index = photosDataSource.fetchResult.indexOfObject(object)
if index != NSNotFound {
mutableIndexSet.addIndex(index)
}
}
// Convert into index paths
let indexPaths = mutableIndexSet.bs_indexPathsForSection(1)
// Loop through them and set them as selected in the collection view
for indexPath in indexPaths {
collectionView.selectItemAtIndexPath(indexPath, animated: false, scrollPosition: .None)
}
}
func updateAlbumTitle(album: PHAssetCollection) {
if let title = album.localizedTitle {
// Update album title
albumTitleView?.albumTitle = title
}
}
func initializePhotosDataSource(album: PHAssetCollection, selections: PHFetchResult? = nil) {
// Set up a photo data source with album
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [
NSSortDescriptor(key: "creationDate", ascending: false)
]
fetchOptions.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.Image.rawValue)
initializePhotosDataSourceWithFetchResult(PHAsset.fetchAssetsInAssetCollection(album, options: fetchOptions), selections: selections)
}
func initializePhotosDataSourceWithFetchResult(fetchResult: PHFetchResult, selections: PHFetchResult? = nil) {
let newDataSource = PhotoCollectionViewDataSource(fetchResult: fetchResult, selections: selections, settings: settings)
// Transfer image size
// TODO: Move image size to settings
if let photosDataSource = photosDataSource {
newDataSource.imageSize = photosDataSource.imageSize
newDataSource.selections = photosDataSource.selections
}
photosDataSource = newDataSource
// Hook up data source
composedDataSource = ComposedCollectionViewDataSource(dataSources: [cameraDataSource, newDataSource])
collectionView?.dataSource = composedDataSource
collectionView?.delegate = self
}
func synchronizeCollectionView() {
guard let collectionView = collectionView else {
return
}
// Reload and sync selections
collectionView.reloadData()
synchronizeSelectionInCollectionView(collectionView)
}
}
// MARK: UICollectionViewDelegate
extension PhotosViewController {
override func collectionView(collectionView: UICollectionView, shouldSelectItemAtIndexPath indexPath: NSIndexPath) -> Bool {
// Camera shouldn't be selected, but pop the UIImagePickerController!
if let composedDataSource = composedDataSource where composedDataSource.dataSources[indexPath.section].isEqual(cameraDataSource) {
let cameraController = UIImagePickerController()
cameraController.allowsEditing = false
cameraController.sourceType = .Camera
cameraController.delegate = self
self.presentViewController(cameraController, animated: true, completion: nil)
return false
}
return collectionView.userInteractionEnabled && photosDataSource!.selections.count < settings.maxNumberOfSelections
}
override func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
guard let photosDataSource = photosDataSource, let cell = collectionView.cellForItemAtIndexPath(indexPath) as? PhotoCell, let asset = photosDataSource.fetchResult.objectAtIndex(indexPath.row) as? PHAsset else {
return
}
// Select asset if not already selected
photosDataSource.selections.append(asset)
// Set selection number
if let selectionCharacter = settings.selectionCharacter {
cell.selectionString = String(selectionCharacter)
} else {
cell.selectionString = String(photosDataSource.selections.count)
}
// Update done button
updateDoneButton()
// Call selection closure
if let closure = selectionClosure {
dispatch_async(dispatch_get_global_queue(0, 0), { () -> Void in
closure(asset: asset)
})
}
}
override func collectionView(collectionView: UICollectionView, didDeselectItemAtIndexPath indexPath: NSIndexPath) {
guard let photosDataSource = photosDataSource, let asset = photosDataSource.fetchResult.objectAtIndex(indexPath.row) as? PHAsset, let index = photosDataSource.selections.indexOf(asset) else {
return
}
// Deselect asset
photosDataSource.selections.removeAtIndex(index)
// Update done button
updateDoneButton()
// Reload selected cells to update their selection number
if let selectedIndexPaths = collectionView.indexPathsForSelectedItems() {
UIView.setAnimationsEnabled(false)
collectionView.reloadItemsAtIndexPaths(selectedIndexPaths)
synchronizeSelectionInCollectionView(collectionView)
UIView.setAnimationsEnabled(true)
}
// Call deselection closure
if let closure = deselectionClosure {
dispatch_async(dispatch_get_global_queue(0, 0), { () -> Void in
closure(asset: asset)
})
}
}
override func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
guard let cell = cell as? CameraCell else {
return
}
cell.startLiveBackground() // Start live background
}
}
// MARK: UIPopoverPresentationControllerDelegate
extension PhotosViewController: UIPopoverPresentationControllerDelegate {
func adaptivePresentationStyleForPresentationController(controller: UIPresentationController) -> UIModalPresentationStyle {
return .None
}
func popoverPresentationControllerShouldDismissPopover(popoverPresentationController: UIPopoverPresentationController) -> Bool {
return true
}
}
// MARK: UINavigationControllerDelegate
extension PhotosViewController: UINavigationControllerDelegate {
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
if operation == .Push {
return expandAnimator
} else {
return shrinkAnimator
}
}
}
// MARK: UITableViewDelegate
extension PhotosViewController: UITableViewDelegate {
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
// Update photos data source
if let album = albumsDataSource.fetchResults[indexPath.section][indexPath.row] as? PHAssetCollection {
initializePhotosDataSource(album)
updateAlbumTitle(album)
synchronizeCollectionView()
}
// Dismiss album selection
albumsViewController.dismissViewControllerAnimated(true, completion: nil)
}
}
// MARK: Traits
extension PhotosViewController {
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
super.traitCollectionDidChange(previousTraitCollection)
if let collectionViewFlowLayout = collectionViewLayout as? GridCollectionViewLayout {
let itemSpacing: CGFloat = 2.0
let cellsPerRow = settings.cellsPerRow(verticalSize: traitCollection.verticalSizeClass, horizontalSize: traitCollection.horizontalSizeClass)
collectionViewFlowLayout.itemSpacing = itemSpacing
collectionViewFlowLayout.itemsPerRow = cellsPerRow
photosDataSource?.imageSize = collectionViewFlowLayout.itemSize
updateDoneButton()
}
}
}
// MARK: UIImagePickerControllerDelegate
extension PhotosViewController: UIImagePickerControllerDelegate {
func imagePickerController(picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : AnyObject]) {
guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage else {
picker.dismissViewControllerAnimated(true, completion: nil)
return
}
var placeholder: PHObjectPlaceholder?
PHPhotoLibrary.sharedPhotoLibrary().performChanges({
let request = PHAssetChangeRequest.creationRequestForAssetFromImage(image)
placeholder = request.placeholderForCreatedAsset
}, completionHandler: { success, error in
guard let placeholder = placeholder, let asset = PHAsset.fetchAssetsWithLocalIdentifiers([placeholder.localIdentifier], options: nil).firstObject as? PHAsset where success == true else {
picker.dismissViewControllerAnimated(true, completion: nil)
return
}
dispatch_async(dispatch_get_main_queue()) {
// TODO: move to a function. this is duplicated in didSelect
self.photosDataSource?.selections.append(asset)
self.updateDoneButton()
// Call selection closure
if let closure = self.selectionClosure {
dispatch_async(dispatch_get_global_queue(0, 0), { () -> Void in
closure(asset: asset)
})
}
picker.dismissViewControllerAnimated(true, completion: nil)
}
})
}
func imagePickerControllerDidCancel(picker: UIImagePickerController) {
picker.dismissViewControllerAnimated(true, completion: nil)
}
}
// MARK: PHPhotoLibraryChangeObserver
extension PhotosViewController: PHPhotoLibraryChangeObserver {
func photoLibraryDidChange(changeInstance: PHChange) {
guard let photosDataSource = photosDataSource, let collectionView = collectionView else {
return
}
dispatch_async(dispatch_get_main_queue(), { () -> Void in
if let photosChanges = changeInstance.changeDetailsForFetchResult(photosDataSource.fetchResult) {
// Update collection view
// Alright...we get spammed with change notifications, even when there are none. So guard against it
if photosChanges.hasIncrementalChanges && (photosChanges.removedIndexes?.count > 0 || photosChanges.insertedIndexes?.count > 0 || photosChanges.changedIndexes?.count > 0) {
// Update fetch result
photosDataSource.fetchResult = photosChanges.fetchResultAfterChanges
if let removed = photosChanges.removedIndexes {
collectionView.deleteItemsAtIndexPaths(removed.bs_indexPathsForSection(1))
}
if let inserted = photosChanges.insertedIndexes {
collectionView.insertItemsAtIndexPaths(inserted.bs_indexPathsForSection(1))
}
// Changes is causing issues right now...fix me later
// Example of issue:
// 1. Take a new photo
// 2. We will get a change telling to insert that asset
// 3. While it's being inserted we get a bunch of change request for that same asset
// 4. It flickers when reloading it while being inserted
// TODO: FIX
// if let changed = photosChanges.changedIndexes {
// print("changed")
// collectionView.reloadItemsAtIndexPaths(changed.bs_indexPathsForSection(1))
// }
// Sync selection
self.synchronizeSelectionInCollectionView(collectionView)
} else if photosChanges.hasIncrementalChanges == false {
// Update fetch result
photosDataSource.fetchResult = photosChanges.fetchResultAfterChanges
collectionView.reloadData()
// Sync selection
self.synchronizeSelectionInCollectionView(collectionView)
}
}
})
// TODO: Changes in albums
}
}