Display camera roll images using Photos.

import UIKit
import Photos
import ImageIO

class ViewController: UIViewController {

    @IBOutlet weak var safeAreaView: UIView!
    @IBOutlet weak var collectionView: UICollectionView!

    private let fetchQueue = DispatchQueue(label: "FetchQueue")

    private var fetchAssets = PHFetchResult<PHAsset>()
    private var albumTitle: String?

    override func viewDidLoad() {
        super.viewDidLoad()
        requestAuthorization()
    }

    private func requestAuthorization() {
        PHPhotoLibrary.requestAuthorization { status in
            DispatchQueue.main.async {
                switch status {
                case .authorized: self.fetchAssetCollections()
                default: print("ERROR: RequestAuthorization")
                }
            }
        }
    }

    private func fetchAssetCollections() {
        if let album = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil).firstObject {
            albumTitle = album.localizedTitle
            fetchAssets = PHAsset.fetchAssets(in: album, options: nil)
            collectionView.reloadData()
        }
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        PHPhotoLibrary.shared().register(self)
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        PHPhotoLibrary.shared().unregisterChangeObserver(self)
    }

}

extension ViewController: PHPhotoLibraryChangeObserver {

    func photoLibraryDidChange(_ changeInstance: PHChange) {
        guard let isChanged = changeInstance.changeDetails(for: fetchAssets) else {return}
        fetchAssets = isChanged.fetchResultAfterChanges
        DispatchQueue.main.async {
            guard isChanged.hasIncrementalChanges else {
                self.collectionView.reloadData()
                return
            }
            self.collectionView.performBatchUpdates({
                if let insert = isChanged.insertedIndexes, !insert.isEmpty {
                    let indexPath = insert.map {IndexPath(item: $0, section: 0)}
                    self.collectionView.insertItems(at: indexPath)
                }
                if let change = isChanged.changedIndexes, !change.isEmpty {
                    let indexPath = change.map {IndexPath(item: $0, section: 0)}
                    self.collectionView.reloadItems(at: indexPath)
                }
                if let remove = isChanged.removedIndexes, !remove.isEmpty {
                    let indexPath = remove.map {IndexPath(item: $0, section: 0)}
                    self.collectionView.deleteItems(at: indexPath)
                }
                isChanged.enumerateMoves { (at, to) in
                    let atIP = IndexPath(item: at, section: 0)
                    let toIP = IndexPath(item: to, section: 0)
                    self.collectionView.moveItem(at: atIP, to: toIP)
                }
            })
        }
    }

}

extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return fetchAssets.count
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        guard let flowLayout = collectionViewLayout as? UICollectionViewFlowLayout else {
            return collectionView.bounds.size
        }
        let cellCount: CGFloat = 4
        let minimumInteritemSpacing = flowLayout.minimumInteritemSpacing * (cellCount - 1)
        let size = floor((collectionView.bounds.width - minimumInteritemSpacing) / cellCount)
        return CGSize(width: size, height: size)
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
        fetchQueue.async {
            self.thumbnail(using: indexPath) {cell.imageView.image = $0}
        }
        return cell
    }

    private func thumbnail(using indexPath: IndexPath, _ completion: ((UIImage?) -> ())?) {
        let asset = self.fetchAssets[indexPath.row]
        asset.requestContentEditingInput(with: nil) { (input, info) in
            guard let url = input?.fullSizeImageURL else {
                completion?(nil)
                return
            }
            guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else {
                completion?(nil)
                return
            }
            let options: [CFString : Any] = [kCGImageSourceThumbnailMaxPixelSize: 125,
                                             kCGImageSourceCreateThumbnailFromImageAlways: true]
            guard let scaledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else {
                completion?(nil)
                return
            }
            let orientation = UInt32(input?.fullSizeImageOrientation ?? 1)
            guard let cgOrientation = CGImagePropertyOrientation(rawValue: orientation) else {
                completion?(nil)
                return
            }
            let uiOrientation = UIImageOrientation(cgOrientation)
            let uiImage = UIImage(cgImage: scaledImage, scale: 1, orientation: uiOrientation)
            completion?(uiImage)
        }
    }

}

class CollectionViewCell: UICollectionViewCell {

    @IBOutlet weak var imageView: UIImageView!

}

extension UIImageOrientation {

    init(_ cgOrientation: CGImagePropertyOrientation) {
        switch cgOrientation {
        case .up: self = .up
        case .upMirrored: self = .upMirrored
        case .down: self = .down
        case .downMirrored: self = .downMirrored
        case .left: self = .left
        case .leftMirrored: self = .leftMirrored
        case .right: self = .right
        case .rightMirrored: self = .rightMirrored
        }
    }

}