255 lines
10 KiB
Swift
255 lines
10 KiB
Swift
/*
|
||
Copyright (C) 2017 Apple Inc. All Rights Reserved.
|
||
See LICENSE.txt for this sample’s licensing information
|
||
|
||
Abstract:
|
||
Manages the second-level collection view, a grid of photos in a collection (or all photos).
|
||
*/
|
||
|
||
|
||
import UIKit
|
||
import Photos
|
||
import PhotosUI
|
||
|
||
private extension UICollectionView {
|
||
func indexPathsForElements(in rect: CGRect) -> [IndexPath] {
|
||
let allLayoutAttributes = collectionViewLayout.layoutAttributesForElements(in: rect)!
|
||
return allLayoutAttributes.map { $0.indexPath }
|
||
}
|
||
}
|
||
|
||
class AssetGridViewController: UICollectionViewController {
|
||
|
||
var fetchResult: PHFetchResult<PHAsset>!
|
||
var assetCollection: PHAssetCollection!
|
||
|
||
@IBOutlet var addButtonItem: UIBarButtonItem!
|
||
|
||
fileprivate let imageManager = PHCachingImageManager()
|
||
fileprivate var thumbnailSize: CGSize!
|
||
fileprivate var previousPreheatRect = CGRect.zero
|
||
|
||
// MARK: UIViewController / Lifecycle
|
||
|
||
override func viewDidLoad() {
|
||
super.viewDidLoad()
|
||
|
||
resetCachedAssets()
|
||
PHPhotoLibrary.shared().register(self)
|
||
|
||
// If we get here without a segue, it's because we're visible at app launch,
|
||
// so match the behavior of segue from the default "All Photos" view.
|
||
if fetchResult == nil {
|
||
let allPhotosOptions = PHFetchOptions()
|
||
allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)]
|
||
fetchResult = PHAsset.fetchAssets(with: allPhotosOptions)
|
||
}
|
||
}
|
||
|
||
deinit {
|
||
PHPhotoLibrary.shared().unregisterChangeObserver(self)
|
||
}
|
||
|
||
override func viewWillAppear(_ animated: Bool) {
|
||
super.viewWillAppear(animated)
|
||
|
||
// Determine the size of the thumbnails to request from the PHCachingImageManager
|
||
let scale = UIScreen.main.scale
|
||
let cellSize = (collectionViewLayout as! UICollectionViewFlowLayout).itemSize
|
||
thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale)
|
||
|
||
// Add button to the navigation bar if the asset collection supports adding content.
|
||
if assetCollection == nil || assetCollection.canPerform(.addContent) {
|
||
navigationItem.rightBarButtonItem = addButtonItem
|
||
} else {
|
||
navigationItem.rightBarButtonItem = nil
|
||
}
|
||
}
|
||
|
||
override func viewDidAppear(_ animated: Bool) {
|
||
super.viewDidAppear(animated)
|
||
updateCachedAssets()
|
||
}
|
||
|
||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||
guard let destination = segue.destination as? AssetViewController
|
||
else { fatalError("unexpected view controller for segue") }
|
||
|
||
let indexPath = collectionView!.indexPath(for: sender as! UICollectionViewCell)!
|
||
destination.asset = fetchResult.object(at: indexPath.item)
|
||
destination.assetCollection = assetCollection
|
||
}
|
||
|
||
// MARK: UICollectionView
|
||
|
||
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
|
||
return fetchResult.count
|
||
}
|
||
|
||
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
|
||
let asset = fetchResult.object(at: indexPath.item)
|
||
|
||
// Dequeue a GridViewCell.
|
||
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: GridViewCell.self), for: indexPath) as? GridViewCell
|
||
else { fatalError("unexpected cell in collection view") }
|
||
|
||
// Add a badge to the cell if the PHAsset represents a Live Photo.
|
||
if asset.mediaSubtypes.contains(.photoLive) {
|
||
cell.livePhotoBadgeImage = PHLivePhotoView.livePhotoBadgeImage(options: .overContent)
|
||
}
|
||
|
||
// Request an image for the asset from the PHCachingImageManager.
|
||
cell.representedAssetIdentifier = asset.localIdentifier
|
||
imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: .aspectFill, options: nil, resultHandler: { image, _ in
|
||
// The cell may have been recycled by the time this handler gets called;
|
||
// set the cell's thumbnail image only if it's still showing the same asset.
|
||
if cell.representedAssetIdentifier == asset.localIdentifier {
|
||
cell.thumbnailImage = image
|
||
}
|
||
})
|
||
|
||
return cell
|
||
|
||
}
|
||
|
||
// MARK: UIScrollView
|
||
|
||
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||
updateCachedAssets()
|
||
}
|
||
|
||
// MARK: Asset Caching
|
||
|
||
fileprivate func resetCachedAssets() {
|
||
imageManager.stopCachingImagesForAllAssets()
|
||
previousPreheatRect = .zero
|
||
}
|
||
|
||
fileprivate func updateCachedAssets() {
|
||
// Update only if the view is visible.
|
||
guard isViewLoaded && view.window != nil else { return }
|
||
|
||
// The preheat window is twice the height of the visible rect.
|
||
let visibleRect = CGRect(origin: collectionView!.contentOffset, size: collectionView!.bounds.size)
|
||
let preheatRect = visibleRect.insetBy(dx: 0, dy: -0.5 * visibleRect.height)
|
||
|
||
// Update only if the visible area is significantly different from the last preheated area.
|
||
let delta = abs(preheatRect.midY - previousPreheatRect.midY)
|
||
guard delta > view.bounds.height / 3 else { return }
|
||
|
||
// Compute the assets to start caching and to stop caching.
|
||
let (addedRects, removedRects) = differencesBetweenRects(previousPreheatRect, preheatRect)
|
||
let addedAssets = addedRects
|
||
.flatMap { rect in collectionView!.indexPathsForElements(in: rect) }
|
||
.map { indexPath in fetchResult.object(at: indexPath.item) }
|
||
let removedAssets = removedRects
|
||
.flatMap { rect in collectionView!.indexPathsForElements(in: rect) }
|
||
.map { indexPath in fetchResult.object(at: indexPath.item) }
|
||
|
||
// Update the assets the PHCachingImageManager is caching.
|
||
imageManager.startCachingImages(for: addedAssets,
|
||
targetSize: thumbnailSize, contentMode: .aspectFill, options: nil)
|
||
imageManager.stopCachingImages(for: removedAssets,
|
||
targetSize: thumbnailSize, contentMode: .aspectFill, options: nil)
|
||
|
||
// Store the preheat rect to compare against in the future.
|
||
previousPreheatRect = preheatRect
|
||
}
|
||
|
||
fileprivate func differencesBetweenRects(_ old: CGRect, _ new: CGRect) -> (added: [CGRect], removed: [CGRect]) {
|
||
if old.intersects(new) {
|
||
var added = [CGRect]()
|
||
if new.maxY > old.maxY {
|
||
added += [CGRect(x: new.origin.x, y: old.maxY,
|
||
width: new.width, height: new.maxY - old.maxY)]
|
||
}
|
||
if old.minY > new.minY {
|
||
added += [CGRect(x: new.origin.x, y: new.minY,
|
||
width: new.width, height: old.minY - new.minY)]
|
||
}
|
||
var removed = [CGRect]()
|
||
if new.maxY < old.maxY {
|
||
removed += [CGRect(x: new.origin.x, y: new.maxY,
|
||
width: new.width, height: old.maxY - new.maxY)]
|
||
}
|
||
if old.minY < new.minY {
|
||
removed += [CGRect(x: new.origin.x, y: old.minY,
|
||
width: new.width, height: new.minY - old.minY)]
|
||
}
|
||
return (added, removed)
|
||
} else {
|
||
return ([new], [old])
|
||
}
|
||
}
|
||
|
||
// MARK: UI Actions
|
||
|
||
@IBAction func addAsset(_ sender: AnyObject?) {
|
||
|
||
// Create a dummy image of a random solid color and random orientation.
|
||
let size = (arc4random_uniform(2) == 0) ?
|
||
CGSize(width: 400, height: 300) :
|
||
CGSize(width: 300, height: 400)
|
||
let renderer = UIGraphicsImageRenderer(size: size)
|
||
let image = renderer.image { context in
|
||
UIColor(hue: CGFloat(arc4random_uniform(100))/100,
|
||
saturation: 1, brightness: 1, alpha: 1).setFill()
|
||
context.fill(context.format.bounds)
|
||
}
|
||
|
||
// Add it to the photo library.
|
||
PHPhotoLibrary.shared().performChanges({
|
||
let creationRequest = PHAssetChangeRequest.creationRequestForAsset(from: image)
|
||
if let assetCollection = self.assetCollection {
|
||
let addAssetRequest = PHAssetCollectionChangeRequest(for: assetCollection)
|
||
addAssetRequest?.addAssets([creationRequest.placeholderForCreatedAsset!] as NSArray)
|
||
}
|
||
}, completionHandler: {success, error in
|
||
if !success { print("error creating asset: \(error)") }
|
||
})
|
||
}
|
||
|
||
}
|
||
|
||
// MARK: PHPhotoLibraryChangeObserver
|
||
extension AssetGridViewController: PHPhotoLibraryChangeObserver {
|
||
func photoLibraryDidChange(_ changeInstance: PHChange) {
|
||
|
||
guard let changes = changeInstance.changeDetails(for: fetchResult)
|
||
else { return }
|
||
|
||
// Change notifications may be made on a background queue. Re-dispatch to the
|
||
// main queue before acting on the change as we'll be updating the UI.
|
||
DispatchQueue.main.sync {
|
||
// Hang on to the new fetch result.
|
||
fetchResult = changes.fetchResultAfterChanges
|
||
if changes.hasIncrementalChanges {
|
||
// If we have incremental diffs, animate them in the collection view.
|
||
guard let collectionView = self.collectionView else { fatalError() }
|
||
collectionView.performBatchUpdates({
|
||
// For indexes to make sense, updates must be in this order:
|
||
// delete, insert, reload, move
|
||
if let removed = changes.removedIndexes, removed.count > 0 {
|
||
collectionView.deleteItems(at: removed.map({ IndexPath(item: $0, section: 0) }))
|
||
}
|
||
if let inserted = changes.insertedIndexes, inserted.count > 0 {
|
||
collectionView.insertItems(at: inserted.map({ IndexPath(item: $0, section: 0) }))
|
||
}
|
||
if let changed = changes.changedIndexes, changed.count > 0 {
|
||
collectionView.reloadItems(at: changed.map({ IndexPath(item: $0, section: 0) }))
|
||
}
|
||
changes.enumerateMoves { fromIndex, toIndex in
|
||
collectionView.moveItem(at: IndexPath(item: fromIndex, section: 0),
|
||
to: IndexPath(item: toIndex, section: 0))
|
||
}
|
||
})
|
||
} else {
|
||
// Reload the collection view if incremental diffs are not available.
|
||
collectionView!.reloadData()
|
||
}
|
||
resetCachedAssets()
|
||
}
|
||
}
|
||
}
|
||
|