Tusker/Tusker/Screens/Asset Picker/AssetCollectionViewControll...

270 lines
11 KiB
Swift

//
// AssetCollectionViewController.swift
// Tusker
//
// Created by Shadowfacts on 1/1/20.
// Copyright © 2020 Shadowfacts. All rights reserved.
//
import UIKit
import Photos
private let reuseIdentifier = "assetCell"
private let cameraReuseIdentifier = "showCameraCell"
protocol AssetCollectionViewControllerDelegate: AnyObject {
func shouldSelectAsset(_ asset: PHAsset) -> Bool
func didSelectAssets(_ assets: [PHAsset])
func captureFromCamera()
}
class AssetCollectionViewController: UIViewController, UICollectionViewDelegate {
weak var delegate: AssetCollectionViewControllerDelegate?
private var collectionView: UICollectionView!
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
private var thumbnailSize: CGSize!
private let imageManager = PHCachingImageManager()
private var fetchResult: PHFetchResult<PHAsset>!
var selectedAssets: [PHAsset] {
return collectionView.indexPathsForSelectedItems?.compactMap { (indexPath) in
guard case let .asset(asset) = dataSource.itemIdentifier(for: indexPath) else { return nil }
return asset
} ?? []
}
init() {
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1/3), heightDimension: .fractionalWidth(1/3))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalWidth(1/3))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: item, count: 3)
group.interItemSpacing = .fixed(4)
let section = NSCollectionLayoutSection(group: group)
let layout = UICollectionViewCompositionalLayout(section: section)
collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
collectionView.delegate = self
view.addSubview(collectionView)
// use the safe area layout guide instead of letting it automatically use the safe area insets
// because otherwise, when presented in a popover with the arrow on the left or right side,
// the collection view content will be cut off by the width of the arrow because the popover
// doesn't respect safe area insets
collectionView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
view.safeAreaLayoutGuide.leadingAnchor.constraint(equalTo: collectionView.leadingAnchor),
view.safeAreaLayoutGuide.trailingAnchor.constraint(equalTo: collectionView.trailingAnchor),
// top ignores safe area because when presented in the sheet container, it simplifies the top content offset
view.topAnchor.constraint(equalTo: collectionView.topAnchor),
// bottom ignores safe area because we want cells to underflow bottom of the screen on notched iPhones
view.bottomAnchor.constraint(equalTo: collectionView.bottomAnchor),
])
view.backgroundColor = .appBackground
collectionView.backgroundColor = .appBackground
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
collectionView.alwaysBounceVertical = true
collectionView.allowsMultipleSelection = true
collectionView.allowsSelection = true
collectionView.allowsFocus = true
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
let controlCell = UICollectionView.CellRegistration<AssetPickerControlCollectionViewCell, Item> { cell, indexPath, itemIdentifier in
switch itemIdentifier {
case .showCamera:
cell.imageView.image = UIImage(systemName: "camera")
cell.label.text = "Take a Photo"
case .changeLimitedSelection:
cell.imageView.image = UIImage(systemName: "photo.on.rectangle.angled")
cell.label.text = "Select More Photos"
case .asset(_):
break
}
}
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
switch item {
case .showCamera, .changeLimitedSelection:
return collectionView.dequeueConfiguredReusableCell(using: controlCell, for: indexPath, item: item)
case let .asset(asset):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! AssetCollectionViewCell
cell.updateUI(asset: asset)
self.imageManager.requestImage(for: asset, targetSize: self.thumbnailSize, contentMode: .aspectFill, options: nil) { (image, _) in
guard let image = image else { return }
DispatchQueue.main.async {
guard cell.assetIdentifier == asset.localIdentifier else { return }
cell.thumbnailImage = image
}
}
return cell
}
})
updateItemsSelectedCount()
if let singleFingerPanGesture = collectionView.gestureRecognizers?.first(where: {
$0.name == "multi-select.singleFingerPanGesture"
}),
let interactivePopGesture = navigationController?.interactivePopGestureRecognizer {
singleFingerPanGesture.require(toFail: interactivePopGesture)
}
PHPhotoLibrary.shared().register(self)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let scale = UIScreen.main.scale
let cellWidth = view.bounds.width / 3
thumbnailSize = CGSize(width: cellWidth * scale, height: cellWidth * scale)
loadAssets()
}
private func loadAssets() {
var items = [Item.showCamera]
switch PHPhotoLibrary.authorizationStatus(for: .readWrite) {
case .notDetermined:
PHPhotoLibrary.requestAuthorization(for: .readWrite) { (_) in
DispatchQueue.main.async {
self.loadAssets()
}
}
return
case .restricted, .denied:
// todo: better UI for this
return
case .authorized:
break
case .limited:
items.append(.changeLimitedSelection)
break
@unknown default:
// who knows, just try anyways
break
}
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
fetchResult = fetchAssets(with: options)
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
snapshot.appendSections([.assets])
fetchResult.enumerateObjects { (asset, _, _) in
items.append(.asset(asset))
}
snapshot.appendItems(items)
dataSource.apply(snapshot, animatingDifferences: false)
}
open func fetchAssets(with options: PHFetchOptions) -> PHFetchResult<PHAsset> {
return PHAsset.fetchAssets(with: options)
}
func updateItemsSelectedCount() {
let selected = collectionView.indexPathsForSelectedItems?.count ?? 0
navigationItem.title = "\(selected) selected"
}
// MARK: UICollectionViewDelegate
func collectionView(_ collectionView: UICollectionView, shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool {
return true
}
func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return false }
if let delegate = delegate,
case let .asset(asset) = item {
return delegate.shouldSelectAsset(asset)
}
return true
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
switch item {
case .showCamera:
collectionView.deselectItem(at: indexPath, animated: false)
delegate?.captureFromCamera()
case .changeLimitedSelection:
// todo: change observer
PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: self)
case .asset(_):
updateItemsSelectedCount()
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
updateItemsSelectedCount()
}
func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard case let .asset(asset) = dataSource.itemIdentifier(for: indexPath) else { return nil }
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: { () -> UIViewController? in
return AssetPreviewViewController(asset: asset)
}, actionProvider: nil)
}
func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
if let indexPath = (configuration.identifier as? NSIndexPath) as IndexPath?,
let cell = collectionView.cellForItem(at: indexPath) as? AssetCollectionViewCell {
let parameters = UIPreviewParameters()
parameters.backgroundColor = .black
return UITargetedPreview(view: cell.imageView, parameters: parameters)
} else {
return nil
}
}
// MARK: - Interaction
@objc func donePressed() {
delegate?.didSelectAssets(selectedAssets)
dismiss(animated: true)
}
}
extension AssetCollectionViewController {
enum Section: Hashable {
case assets
}
enum Item: Hashable {
case showCamera
case changeLimitedSelection
case asset(PHAsset)
}
}
extension AssetCollectionViewController: PHPhotoLibraryChangeObserver {
func photoLibraryDidChange(_ changeInstance: PHChange) {
DispatchQueue.main.async {
self.loadAssets()
}
}
}