Learn-iOS-Swift-by-Examples/Photos/Shared/AssetGridViewController.swift

255 lines
10 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
Copyright (C) 2017 Apple Inc. All Rights Reserved.
See LICENSE.txt for this samples 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()
}
}
}