import UIKit import Photos import PhotosUI class ViewController: UIViewController, UICollectionViewDelegate { enum Section: CaseIterable { case main } var fetchResult: PHFetchResult! var dataSource: UICollectionViewDiffableDataSource! var collectionView: UICollectionView! var emptyAlbumMessageView : UIView! = nil let imageManager = PHCachingImageManager() var selectedAssets: [PHAsset] { var pAssets = [PHAsset]() fetchResult.enumerateObjects { (asset, index, stop) in pAssets.append(asset) } return pAssets } override func viewDidLoad() { super.viewDidLoad() } override func viewWillAppear(_ animated: Bool) { createEmptyAlbumMessageView() configurePhotoData() configureHierarchy() configureDataSource() displayPhotos(fetchResult!, title: "All Photos") } func createEmptyAlbumMessageView() { emptyAlbumMessageView = UIView() emptyAlbumMessageView.backgroundColor = .black view.addSubview(emptyAlbumMessageView) emptyAlbumMessageView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ emptyAlbumMessageView.leadingAnchor.constraint(equalTo: view.leadingAnchor), emptyAlbumMessageView.trailingAnchor.constraint(equalTo: view.trailingAnchor), emptyAlbumMessageView.topAnchor.constraint(equalTo: view.topAnchor), emptyAlbumMessageView.bottomAnchor.constraint(equalTo: view.bottomAnchor) ]) // Title Label let titleLabel : UILabel = UILabel() titleLabel.text = "Empty Album" titleLabel.textAlignment = .center titleLabel.font = UIFont.boldSystemFont(ofSize: 21.0) emptyAlbumMessageView.addSubview(titleLabel) titleLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ titleLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 100), titleLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 44), titleLabel.centerXAnchor.constraint(equalTo: emptyAlbumMessageView.centerXAnchor, constant: -30), titleLabel.centerYAnchor.constraint(equalTo: emptyAlbumMessageView.centerYAnchor)]) // Message Label let messageLabel : UILabel = UILabel(frame: CGRect(x: 290, y: 394, width: 294, height: 80)) messageLabel.text = "This album is empty. Add some photos to it in the Photos app and they will appear here automatically." messageLabel.font = UIFont.systemFont(ofSize: 17.0) messageLabel.numberOfLines = 3 messageLabel.textAlignment = .center messageLabel.lineBreakMode = .byWordWrapping emptyAlbumMessageView.addSubview(messageLabel) messageLabel.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ messageLabel.widthAnchor.constraint(equalToConstant: 294), messageLabel.heightAnchor.constraint(greaterThanOrEqualToConstant: 80), messageLabel.centerXAnchor.constraint(equalTo: titleLabel.centerXAnchor), messageLabel.firstBaselineAnchor.constraint(equalTo: titleLabel.lastBaselineAnchor, constant: 10) ]) self.view.bringSubviewToFront(emptyAlbumMessageView) self.emptyAlbumMessageView.isHidden = true } func configurePhotoData() { let allPhotosOptions = PHFetchOptions() allPhotosOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: true)] allPhotosOptions.predicate = NSPredicate(format: "mediaType = %d", PHAssetMediaType.image.rawValue) fetchResult = PHAsset.fetchAssets(with: allPhotosOptions) } func configureHierarchy() { let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout()) collectionView.delegate = self view.addSubview(collectionView) collectionView.translatesAutoresizingMaskIntoConstraints = false if #available(iOS 11.0, *) { let safeArea = self.view.safeAreaLayoutGuide collectionView.topAnchor.constraint(equalTo: safeArea.topAnchor, constant: 0).isActive = true } else { let topGuide = self.topLayoutGuide collectionView.topAnchor.constraint(equalTo: topGuide.bottomAnchor, constant: 0).isActive = true } collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true self.collectionView = collectionView self.collectionView.scrollsToTop = false } func configureDataSource() { let cellRegistration = UICollectionView.CellRegistration { [weak self] cell, indexPath, asset in guard let self = self else { return } let scale = UIScreen.main.scale cell.contentMode = .scaleAspectFit let imageViewFrameWidth = self.collectionView.frame.width let imageViewFrameHeight = (Double(asset.pixelHeight)/scale) / (Double(asset.pixelWidth)/scale) * imageViewFrameWidth let thumbnailSize = CGSize(width: imageViewFrameWidth * scale, height: imageViewFrameHeight * scale) cell.representedAssetIdentifier = asset.localIdentifier self.imageManager.requestImage(for: asset, targetSize: thumbnailSize, contentMode: cell.contentMode == .scaleAspectFit ? .aspectFit : .aspectFill, options: nil, resultHandler: { image, _ in if cell.representedAssetIdentifier == asset.localIdentifier { cell.image = image } }) cell.layoutIfNeeded() } dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { (collectionView: UICollectionView, indexPath: IndexPath, asset: PHAsset) -> UICollectionViewCell? in let cell = collectionView.dequeueConfiguredReusableCell(using: cellRegistration, for: indexPath, item: asset) return cell } } func createLayout() -> UICollectionViewLayout { let layout = UICollectionViewCompositionalLayout { (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection in let estimateHeight : Float = 200 let itemWidthDimension = NSCollectionLayoutDimension.fractionalWidth(1.0) let itemHeightDimension = NSCollectionLayoutDimension.estimated(CGFloat(estimateHeight)) let itemSize = NSCollectionLayoutSize(widthDimension: itemWidthDimension, heightDimension: itemHeightDimension) let itemLayout = NSCollectionLayoutItem(layoutSize: itemSize) let groupWidthDimension = NSCollectionLayoutDimension.fractionalWidth(1.0) let groupHeightDimension = NSCollectionLayoutDimension.estimated(CGFloat(estimateHeight)) let groupSize = NSCollectionLayoutSize(widthDimension: groupWidthDimension, heightDimension: groupHeightDimension ) let groupLayout = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [itemLayout]) let sectionLayout = NSCollectionLayoutSection(group: groupLayout) return sectionLayout } return layout } public func displayPhotos(_ fetchResult: PHFetchResult, title: String?) { self.fetchResult = fetchResult self.title = title updateSnapshot(animate: false) scrollToBottom() } func updateSnapshot(animate: Bool = false, reload: Bool = true) { self.emptyAlbumMessageView.isHidden = !(0 == fetchResult.count) var snapshot = NSDiffableDataSourceSnapshot() snapshot.appendSections([.main]) let selectedAssets = selectedAssets snapshot.appendItems(selectedAssets) if true == reload { snapshot.reloadItems(selectedAssets) } else { snapshot.reconfigureItems(selectedAssets) } dataSource.apply(snapshot, animatingDifferences: animate) } public func scrollToBottom() { collectionView.layoutIfNeeded() DispatchQueue.main.async { [self] in self.collectionView!.scrollToItem(at: IndexPath(row: fetchResult.count-1, section: 0), at: .bottom, animated: false) } } } class PhotoThumbnailCollectionViewCell: UICollectionViewCell { var image: UIImage? { didSet { setNeedsUpdateConfiguration() } } override init(frame: CGRect) { super.init(frame: frame) } override func updateConfiguration(using state: UICellConfigurationState) { var config = PhotoThumbnailCellConfiguration().updated(for: state) config.image = image config.contentMode = self.contentMode contentConfiguration = config } var representedAssetIdentifier: String! required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } struct PhotoThumbnailCellConfiguration : UIContentConfiguration { var text : String? = nil var image: UIImage? = nil var contentMode : UIView.ContentMode = .scaleAspectFit func makeContentView() -> UIView & UIContentView { return PhotoThumbnailContentView(self) } func updated(for state: UIConfigurationState) -> PhotoThumbnailCellConfiguration { return self } } class PhotoThumbnailContentView : UIView, UIContentView { var configuration: UIContentConfiguration { didSet { self.configure(configuration: configuration) } } let imageView = UIImageView() init(_ configuration: UIContentConfiguration) { self.configuration = configuration super.init(frame:.zero) self.addSubview(self.imageView) imageView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ imageView.topAnchor.constraint(equalTo: self.topAnchor, constant: 0), imageView.bottomAnchor.constraint(equalTo: self.bottomAnchor, constant: 0), imageView.leadingAnchor.constraint(equalTo: self.leadingAnchor, constant: 0), imageView.trailingAnchor.constraint(equalTo: self.trailingAnchor, constant: 0), ]) self.configure(configuration: configuration) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func configure(configuration: UIContentConfiguration) { guard let configuration = configuration as? PhotoThumbnailCellConfiguration else { return } imageView.image = configuration.image imageView.contentMode = configuration.contentMode } }