216 lines
8.8 KiB
Swift
216 lines
8.8 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: class {
|
|
func shouldSelectAsset(_ asset: PHAsset) -> Bool
|
|
func didSelectAssets(_ assets: [PHAsset])
|
|
func captureFromCamera()
|
|
}
|
|
|
|
class AssetCollectionViewController: UICollectionViewController {
|
|
|
|
weak var delegate: AssetCollectionViewControllerDelegate?
|
|
|
|
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
|
private var flowLayout: UICollectionViewFlowLayout {
|
|
return collectionViewLayout as! UICollectionViewFlowLayout
|
|
}
|
|
|
|
private var availableWidth: CGFloat!
|
|
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(collectionViewLayout: UICollectionViewFlowLayout())
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
|
|
// 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 = .systemBackground
|
|
|
|
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed))
|
|
|
|
collectionView.alwaysBounceVertical = true
|
|
|
|
collectionView.register(UINib(nibName: "AssetCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: reuseIdentifier)
|
|
collectionView.register(UINib(nibName: "ShowCameraCollectionViewCell", bundle: .main), forCellWithReuseIdentifier: cameraReuseIdentifier)
|
|
|
|
let scale = UIScreen.main.scale
|
|
let cellSize = flowLayout.itemSize
|
|
thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale)
|
|
|
|
dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
|
|
switch item {
|
|
case .showCamera:
|
|
return collectionView.dequeueReusableCell(withReuseIdentifier: cameraReuseIdentifier, for: indexPath)
|
|
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
|
|
}
|
|
})
|
|
|
|
let options = PHFetchOptions()
|
|
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
|
|
fetchResult = fetchAssets(with: options)
|
|
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
|
snapshot.appendSections([.assets])
|
|
var items: [Item] = [.showCamera]
|
|
fetchResult.enumerateObjects { (asset, _, _) in
|
|
items.append(.asset(asset))
|
|
}
|
|
snapshot.appendItems(items)
|
|
dataSource.apply(snapshot, animatingDifferences: false)
|
|
|
|
collectionView.allowsMultipleSelection = true
|
|
setEditing(true, animated: false)
|
|
|
|
updateItemsSelectedCount()
|
|
|
|
if let singleFingerPanGesture = collectionView.gestureRecognizers?.first(where: {
|
|
$0.name == "multi-select.singleFingerPanGesture"
|
|
}),
|
|
let interactivePopGesture = navigationController?.interactivePopGestureRecognizer {
|
|
singleFingerPanGesture.require(toFail: interactivePopGesture)
|
|
}
|
|
}
|
|
|
|
override func viewWillLayoutSubviews() {
|
|
super.viewWillLayoutSubviews()
|
|
|
|
let availableWidth = view.bounds.inset(by: view.safeAreaInsets).width
|
|
|
|
if self.availableWidth != availableWidth {
|
|
self.availableWidth = availableWidth
|
|
|
|
let size = (availableWidth - 8) / 3
|
|
flowLayout.itemSize = CGSize(width: size, height: size)
|
|
flowLayout.minimumInteritemSpacing = 4
|
|
flowLayout.minimumLineSpacing = 4
|
|
}
|
|
}
|
|
|
|
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
|
|
|
|
override func collectionView(_ collectionView: UICollectionView, shouldBeginMultipleSelectionInteractionAt indexPath: IndexPath) -> Bool {
|
|
return true
|
|
}
|
|
|
|
override 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
|
|
}
|
|
|
|
override 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 .asset(_):
|
|
updateItemsSelectedCount()
|
|
}
|
|
}
|
|
|
|
override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
|
|
updateItemsSelectedCount()
|
|
}
|
|
|
|
override 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)
|
|
}
|
|
|
|
override 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 asset(PHAsset)
|
|
}
|
|
}
|