iOS与多线程(七) —— GCD之一个简单应用示例源码(三)

版本记录

版本号 时间
V1.0 2018.08.21

前言

信号量机制是多线程通信中的比较重要的一部分,对于NSOperation可以设置并发数,但是对于GCD就不能设置并发数了,那么就只能靠信号量机制了。接下来这几篇就会详细的说一下并发机制。感兴趣的可以看这几篇文章。
1. iOS与多线程(一) —— GCD中的信号量及几个重要函数
2. iOS与多线程(二) —— NSOperation实现多并发之创建任务
3. iOS与多线程(三) —— NSOperation实现多并发之创建队列和开启线程
4. iOS与多线程(四) —— NSOperation的串并行和操作依赖
5. iOS与多线程(五) —— GCD之一个简单应用示例(一)
6. iOS与多线程(六) —— GCD之一个简单应用示例(二)

源码

1. PhotoCollectionViewController.swift
import UIKit
import Photos

private let reuseIdentifier = "photoCell"
private let backgroundImageOpacity: CGFloat = 0.1

final class PhotoCollectionViewController: UICollectionViewController {
  // MARK: - Lifecycle
  override func viewDidLoad() {
    super.viewDidLoad()
    
    let backgroundImageView = UIImageView(image: UIImage(named:"background"))
    backgroundImageView.alpha = backgroundImageOpacity
    backgroundImageView.contentMode = .center
    collectionView?.backgroundView = backgroundImageView
    
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(contentChangedNotification(_:)),
      name: PhotoManagerNotification.contentUpdated,
      object: nil)
    
    NotificationCenter.default.addObserver(
      self,
      selector: #selector(contentChangedNotification(_:)),
      name: PhotoManagerNotification.contentAdded,
      object: nil)
  }
  
  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    showOrHideNavPrompt()
  }

  // MARK: - IBAction Methods
  @IBAction func addPhotoAssets(_ sender: Any) {
    let alert = UIAlertController(title: "Get Photos From:", message: nil, preferredStyle: .actionSheet)
    
    let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
    alert.addAction(cancelAction)
    
    let libraryAction = UIAlertAction(title: "Photo Library", style: .default) { _ in
      let viewController = self.storyboard?.instantiateViewController(withIdentifier: "AlbumsStoryboard") as? UINavigationController
      if let viewController = viewController,
        let albumsTableViewController = viewController.topViewController as? AlbumsTableViewController {
        albumsTableViewController.assetPickerDelegate = self
        self.present(viewController, animated: true, completion: nil)
      }
    }
    alert.addAction(libraryAction)
    
    let internetAction = UIAlertAction(title: "Le Internet", style: .default) { _ in
      self.downloadImageAssets()
    }
    alert.addAction(internetAction)
    
    present(alert, animated: true, completion: nil)
  }
}

// MARK: - Private Methods
private extension PhotoCollectionViewController {
  func showOrHideNavPrompt() {
    let delayInSeconds = 2.0
    DispatchQueue.main.asyncAfter(deadline: .now() + delayInSeconds) { [weak self] in
      guard let self = self else {
        return
      }
      
      if PhotoManager.shared.photos.count > 0 {
        self.navigationItem.prompt = nil
      } else {
        self.navigationItem.prompt = "Add photos with faces to Googlyify them!"
      }
      self.navigationController?.viewIfLoaded?.setNeedsLayout()
    }
  }
  
  func downloadImageAssets() {
    PhotoManager.shared.downloadPhotos() { [weak self] error in
      
      let message = error?.localizedDescription ?? "The images have finished downloading"
      let alert = UIAlertController(title: "Download Complete", message: message, preferredStyle: .alert)
      alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
      self?.present(alert, animated: true, completion: nil)
    }
  }
}

// MARK: - Notification handlers
extension PhotoCollectionViewController {
  @objc func contentChangedNotification(_ notification: Notification!) {
    collectionView?.reloadData()
    showOrHideNavPrompt()
  }
}

// MARK: - UICollectionViewDataSource
extension PhotoCollectionViewController {
  override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return PhotoManager.shared.photos.count
  }
  
  override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! PhotoCollectionViewCell
    
    let photoAssets = PhotoManager.shared.photos
    let photo = photoAssets[indexPath.row]
    
    switch photo.statusThumbnail {
    case .goodToGo:
      cell.thumbnailImage = photo.thumbnail
    case .downloading:
      cell.thumbnailImage = UIImage(named: "photoDownloading")
    case .failed:
      cell.thumbnailImage = UIImage(named: "photoDownloadError")
    }
    
    return cell
  }
}

// MARK: - UICollectionViewDelegate
extension PhotoCollectionViewController {
  override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    let photos = PhotoManager.shared.photos
    let photo = photos[indexPath.row]
    
    switch photo.statusImage {
    case .goodToGo:
      let viewController = storyboard?.instantiateViewController(withIdentifier: "PhotoDetailStoryboard") as? PhotoDetailViewController
      if let viewController = viewController {
        viewController.image = photo.image
        navigationController?.pushViewController(viewController, animated: true)
      }
      
    case .downloading:
      let alert = UIAlertController(title: "Downloading",
                                    message: "The image is currently downloading",
                                    preferredStyle: .alert)
      alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
      present(alert, animated: true, completion: nil)
      
    case .failed:
      let alert = UIAlertController(title: "Image Failed",
                                    message: "The image failed to be created",
                                    preferredStyle: .alert)
      alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
      present(alert, animated: true, completion: nil)
    }
  }
}

// MARK: - AssetPickerDelegate

extension PhotoCollectionViewController: AssetPickerDelegate {
  func assetPickerDidCancel() {
    dismiss(animated: true, completion: nil)
  }
  
  func assetPickerDidFinishPickingAssets(_ selectedAssets: [PHAsset])  {
    for asset in selectedAssets {
      let photo = AssetPhoto(asset: asset)
      PhotoManager.shared.addPhoto(photo)
    }

    dismiss(animated: true, completion: nil)
  }
}
2. PhotoCollectionViewCell.swift
import UIKit

final class PhotoCollectionViewCell: UICollectionViewCell {
  @IBOutlet var imageView: UIImageView!
  var representedAssetIdentifier: String!
  var thumbnailImage: UIImage! {
    didSet {
      imageView.image = thumbnailImage
    }
  }
  
  override func prepareForReuse() {
    super.prepareForReuse()
    imageView.image = nil
  }
}
3. PhotoDetailViewController.swift
import UIKit

private struct ScaleFactor {
  static let retinaToEye: CGFloat = 0.5
  static let faceBoundsToEye: CGFloat = 4.0
}

final class PhotoDetailViewController: UIViewController {
  @IBOutlet weak var photoImageView: UIImageView!
  
  var image: UIImage!
  
  // MARK: - Lifecycle
  override func viewDidLoad() {
    super.viewDidLoad()
    
    assert(image != nil, "Image not set; required to use view controller")
    photoImageView.image = image
    
    // Resize if neccessary to ensure it's not pixelated
    if image.size.height <= photoImageView.bounds.size.height &&
      image.size.width <= photoImageView.bounds.size.width {
      photoImageView.contentMode = .center
    }
    
    DispatchQueue.global(qos: .userInitiated).async { [weak self] in
      guard let self = self else {
        return
      }
      let overlayImage = self.faceOverlayImageFrom(self.image)
      DispatchQueue.main.async { [weak self] in
        self?.fadeInNewImage(overlayImage)
      }
    }
  }

}

// MARK: - Private Methods

private extension PhotoDetailViewController {
  func faceOverlayImageFrom(_ image: UIImage) -> UIImage {
    let detector = CIDetector(ofType: CIDetectorTypeFace,
        context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
    
    // Get features from the image
    let newImage = CIImage(cgImage: image.cgImage!)
    let features = detector?.features(in: newImage) as! [CIFaceFeature]
    
    UIGraphicsBeginImageContext(image.size)
    let imageRect = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
    
    // Draws this in the upper left coordinate system
    image.draw(in: imageRect, blendMode: .normal, alpha: 1.0)
    
    let context = UIGraphicsGetCurrentContext()
    for faceFeature in features {
      let faceRect = faceFeature.bounds
      context!.saveGState()
      
      // CI and CG work in different coordinate systems, we should translate to
      // the correct one so we don't get mixed up when calculating the face position.
      context!.translateBy(x: 0.0, y: imageRect.size.height)
      context!.scaleBy(x: 1.0, y: -1.0)
      
      if faceFeature.hasLeftEyePosition {
        let leftEyePosition = faceFeature.leftEyePosition
        let eyeWidth = faceRect.size.width / ScaleFactor.faceBoundsToEye
        let eyeHeight = faceRect.size.height / ScaleFactor.faceBoundsToEye
        let eyeRect = CGRect(x: leftEyePosition.x - eyeWidth / 2.0,
                             y: leftEyePosition.y - eyeHeight / 2.0,
                             width: eyeWidth,
                             height: eyeHeight)
        drawEyeBallForFrame(eyeRect)
      }
      
      if faceFeature.hasRightEyePosition {
        let leftEyePosition = faceFeature.rightEyePosition
        let eyeWidth = faceRect.size.width / ScaleFactor.faceBoundsToEye
        let eyeHeight = faceRect.size.height / ScaleFactor.faceBoundsToEye
        let eyeRect = CGRect(x: leftEyePosition.x - eyeWidth / 2.0,
                             y: leftEyePosition.y - eyeHeight / 2.0,
                             width: eyeWidth,
                             height: eyeHeight)
        drawEyeBallForFrame(eyeRect)
      }
      
      context!.restoreGState();
    }
    
    let overlayImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
    
    return overlayImage!
  }
  
  func faceRotationInRadians(leftEyePoint startPoint: CGPoint, rightEyePoint endPoint: CGPoint) -> CGFloat {
    let deltaX = endPoint.x - startPoint.x
    let deltaY = endPoint.y - startPoint.y
    let angleInRadians = CGFloat(atan2f(Float(deltaY), Float(deltaX)))
    
    return angleInRadians;
  }
  
  func drawEyeBallForFrame(_ rect: CGRect) {
    let context = UIGraphicsGetCurrentContext()
    context?.addEllipse(in: rect)
    context?.setFillColor(UIColor.white.cgColor)
    context?.fillPath()
    
    let eyeSizeWidth = rect.size.width * ScaleFactor.retinaToEye
    let eyeSizeHeight = rect.size.height * ScaleFactor.retinaToEye
    
    var x = CGFloat(arc4random_uniform(UInt32(rect.size.width - eyeSizeWidth)))
    var y = CGFloat(arc4random_uniform(UInt32(rect.size.height - eyeSizeHeight)))
    x += rect.origin.x
    y += rect.origin.y
    
    let eyeSize = min(eyeSizeWidth, eyeSizeHeight)
    let eyeBallRect = CGRect(x: x, y: y, width: eyeSize, height: eyeSize)
    context?.addEllipse(in: eyeBallRect)
    context?.setFillColor(UIColor.black.cgColor)
    context?.fillPath()
  }
  
  func fadeInNewImage(_ newImage: UIImage) {
    let tmpImageView = UIImageView(image: newImage)
    tmpImageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    tmpImageView.contentMode = photoImageView.contentMode
    tmpImageView.frame = photoImageView.bounds
    tmpImageView.alpha = 0.0
    photoImageView.addSubview(tmpImageView)
    
    UIView.animate(withDuration: 0.75, animations: {
      tmpImageView.alpha = 1.0
    }, completion: { finished in
      self.photoImageView.image = newImage
      tmpImageView.removeFromSuperview()
    })
  }
}
4. Photo.swift
import UIKit
import Photos

typealias PhotoDownloadCompletionBlock = (_ image: UIImage?, _ error: NSError?) -> Void
typealias PhotoDownloadProgressBlock = (_ completed: Int, _ total: Int) -> Void

enum PhotoStatus {
  case downloading
  case goodToGo
  case failed
}

private let scale = UIScreen.main.scale
private let cellSize = CGSize(width: 64, height: 64)
private let thumbnailSize = CGSize(width: cellSize.width * scale, height: cellSize.height * scale)
private let imageManager = PHImageManager()

protocol Photo {
  var statusImage: PhotoStatus { get }
  var statusThumbnail: PhotoStatus { get }
  var image: UIImage? { get }
  var thumbnail: UIImage? { get }
}

class AssetPhoto: Photo {
  var statusImage: PhotoStatus = .downloading
  var statusThumbnail: PhotoStatus = .downloading
  var image: UIImage?
  var thumbnail: UIImage?
  
  let asset: PHAsset
  let representedAssetIdentifier: String
  private let targetSize: CGSize = CGSize(width:600, height:600)
  
  init(asset: PHAsset) {
    self.asset = asset
    representedAssetIdentifier = asset.localIdentifier
    fetchThumbnailImage()
    fetchImage()
  }
  
  private func fetchThumbnailImage() {
    let options = PHImageRequestOptions()
    options.isNetworkAccessAllowed = true
    imageManager.requestImage(for: asset, targetSize: thumbnailSize,
        contentMode: .aspectFill, options: options) { image, info in
      if let image = image {
        if self.representedAssetIdentifier == self.asset.localIdentifier {
          self.thumbnail = image
          self.statusThumbnail = .goodToGo
        }
      } else if let info = info,
        let _ = info[PHImageErrorKey] as? NSError {
        self.statusThumbnail = .failed
      }
    }
  }
  
  private func fetchImage() {
    let options = PHImageRequestOptions()
    options.deliveryMode = .highQualityFormat
    options.isNetworkAccessAllowed = true
    
    PHImageManager.default().requestImage(for: asset, targetSize: targetSize,
        contentMode: .aspectFill, options: options) { image, info in
      if let image = image {
        if image.size.width < self.targetSize.width / 2 {
          DispatchQueue.main.asyncAfter(deadline: .now(), execute: self.fetchImage)
        }
        if self.representedAssetIdentifier == self.asset.localIdentifier {
          self.image = image
          self.statusImage = .goodToGo
        }
      } else if let info = info,
        let _ = info[PHImageErrorKey] as? NSError {
        self.statusImage = .failed
      }
    }
  }
}

private let downloadSession = URLSession(configuration: URLSessionConfiguration.ephemeral)

class DownloadPhoto: Photo {
  var statusImage: PhotoStatus = .downloading
  var statusThumbnail: PhotoStatus = .downloading
  var image: UIImage?
  var thumbnail: UIImage?
  
  let url: URL
  
  init(url: URL, completion: PhotoDownloadCompletionBlock?) {
    self.url = url
    downloadImage(completion)
  }
  
  convenience init(url: URL) {
    self.init(url: url, completion: nil)
  }
  
  private func downloadImage(_ completion: PhotoDownloadCompletionBlock?) {
    let task = downloadSession.dataTask(with: url) { data, response, error in
      if let data = data {
        self.image = UIImage(data: data)
      }
      if error == nil && self.image != nil {
        self.statusImage = .goodToGo
        self.statusThumbnail = .goodToGo
      } else {
        self.statusImage = .failed
        self.statusThumbnail = .failed
      }
      
      self.thumbnail = self.image?.thumbnailImage(Int(cellSize.width), transparentBorder: 0,
          cornerRadius: 0, interpolationQuality: CGInterpolationQuality.default)
      
      completion?(self.image, error as NSError?)
      
      DispatchQueue.main.async {
        NotificationCenter.default.post(name: PhotoManagerNotification.contentUpdated, object: nil)
      }
    }
    
    task.resume()
  }
}
5. PhotoManager.swift
import UIKit

struct PhotoManagerNotification {
  // Notification when new photo instances are added
  static let contentAdded = Notification.Name("com.raywenderlich.GooglyPuff.PhotoManagerContentAdded")
  // Notification when content updates (i.e. Download finishes)
  static let contentUpdated = Notification.Name("com.raywenderlich.GooglyPuff.PhotoManagerContentUpdated")
}

struct PhotoURLString {
  // Photo Credit: Devin Begley, http://www.devinbegley.com/
  static let overlyAttachedGirlfriend = "https://i.imgur.com/UvqEgCv.png"
  static let successKid = "https://i.imgur.com/dZ5wRtb.png"
  static let lotsOfFaces = "https://i.imgur.com/tPzTg7A.jpg"
}

typealias PhotoProcessingProgressClosure = (_ completionPercentage: CGFloat) -> Void
typealias BatchPhotoDownloadingCompletionClosure = (_ error: NSError?) -> Void

class PhotoManager {
  private init() {}
  static let shared = PhotoManager()
  
  private let concurrentPhotoQueue =
    DispatchQueue(
      label: "com.raywenderlich.GooglyPuff.photoQueue",
      attributes: .concurrent)
  
  private var unsafePhotos: [Photo] = []
  
  var photos: [Photo] {
    var photosCopy: [Photo] = []
    concurrentPhotoQueue.sync {
      photosCopy = self.unsafePhotos
    }
    return photosCopy
  }
  
  func addPhoto(_ photo: Photo) {
    concurrentPhotoQueue.async(flags: .barrier) { [weak self] in
      guard let self = self else {
        return
      }
      self.unsafePhotos.append(photo)
      DispatchQueue.main.async { [weak self] in
        self?.postContentAddedNotification()
      }
    }
  }
  
  func downloadPhotos(withCompletion completion: BatchPhotoDownloadingCompletionClosure?) {
    var storedError: NSError?
    for address in [PhotoURLString.overlyAttachedGirlfriend,
                    PhotoURLString.successKid,
                    PhotoURLString.lotsOfFaces] {
                      let url = URL(string: address)
                      let photo = DownloadPhoto(url: url!) { _, error in
                        if error != nil {
                          storedError = error
                        }
                      }
                      PhotoManager.shared.addPhoto(photo)
    }
    
    completion?(storedError)
  }
  
  private func postContentAddedNotification() {
    NotificationCenter.default.post(name: PhotoManagerNotification.contentAdded, object: nil)
  }
}
6. AssetPickerDelegate.swift
import Foundation
import Photos

protocol AssetPickerDelegate {
  func assetPickerDidFinishPickingAssets(_ selectedAssets: [PHAsset])
  func assetPickerDidCancel()
}
7. AlbumsTableViewController.swift
import UIKit
import Photos

private let reuseIdentifier = "AlbumsCell"

class AlbumsTableViewController: UITableViewController {

  var selectedAssets: SelectedAssets?
  var assetPickerDelegate: AssetPickerDelegate?
  
  private let sectionNames = ["","Albums"]
  private var userLibrary: PHFetchResult<PHAssetCollection>!
  private var userAlbums: PHFetchResult<PHCollection>!
  
  private var doneButton: UIBarButtonItem!
  
  // MARK: - Lifecycle
  override func viewDidLoad() {
    super.viewDidLoad()
    if selectedAssets == nil {
      selectedAssets = SelectedAssets()
    }
    PHPhotoLibrary.requestAuthorization { status in
      DispatchQueue.main.async {
        switch status {
        case .authorized:
          self.fetchCollections()
          self.tableView.reloadData()
        default:
          self.showNoAccessAlertAndCancel()
        }
      }
    }
    doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(donePressed(_:)))
  }
  
  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    updateDoneButton()
  }
  
  // MARK: - Navigation

  override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    let destination = segue.destination
      as! AssetsCollectionViewController
    // Set up AssetCollectionViewController
    destination.selectedAssets = selectedAssets
    let cell = sender as! UITableViewCell
    destination.title = cell.textLabel!.text
    destination.assetPickerDelegate = self
    let options = PHFetchOptions()
    options.sortDescriptors =
      [NSSortDescriptor(key: "creationDate", ascending: true)]
    let indexPath = tableView.indexPath(for: cell)!
    switch (indexPath.section) {
    case 0:
      // Camera Roll
      let library = userLibrary[indexPath.row]
      destination.assetsFetchResults =
        PHAsset.fetchAssets(in: library,
                            options: options) as? PHFetchResult<AnyObject>
    case 1:
      // Albums
      let album = userAlbums[indexPath.row] as! PHAssetCollection
      destination.assetsFetchResults =
        PHAsset.fetchAssets(in: album,
                            options: options) as? PHFetchResult<AnyObject>
    default:
      break
    }
  }

  @IBAction func cancelPressed(_ sender: Any) {
    assetPickerDelegate?.assetPickerDidCancel()
  }
  
  @IBAction func donePressed(_ sender: Any) {
    // Should only be invoked when there are selected assets
    if let assets = selectedAssets?.assets {
      assetPickerDelegate?.assetPickerDidFinishPickingAssets(assets)
      // Clear out selections
      selectedAssets?.assets.removeAll()
    }
  }
  
}

// MARK: - Private Methods

extension AlbumsTableViewController {
  func fetchCollections() {
    userAlbums = PHCollectionList.fetchTopLevelUserCollections(with: nil)
    userLibrary = PHAssetCollection.fetchAssetCollections(
      with: .smartAlbum,
      subtype: .smartAlbumUserLibrary,
      options: nil)
  }
  
  func showNoAccessAlertAndCancel() {
    let alert = UIAlertController(title: "No Photo Permissions", message: "Please grant photo permissions in Settings", preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: nil))
    alert.addAction(UIAlertAction(title: "Settings", style: .default, handler: { _ in
      UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
      return
    }))
    
    self.present(alert, animated: true, completion: nil)
  }
  
  private func updateDoneButton() {
    guard let selectedAssets = selectedAssets else { return }
    // Add a done button when there are selected assets
    if selectedAssets.assets.count > 0 {
      navigationItem.rightBarButtonItem = doneButton
    } else {
      navigationItem.rightBarButtonItem = nil
    }
  }
}

// MARK: - Table view data source

extension AlbumsTableViewController {
  override func numberOfSections(in tableView: UITableView) -> Int {
    return sectionNames.count
  }
  
  override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    switch (section) {
    case 0:
      return userLibrary?.count ?? 0
    case 1:
      return userAlbums?.count ?? 0
    default:
      return 0
    }
  }
  
  override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: reuseIdentifier, for: indexPath)
    cell.textLabel!.text = ""
    
    switch(indexPath.section) {
    case 0:
      let library = userLibrary[indexPath.row]
      var title = library.localizedTitle!
      if (library.estimatedAssetCount != NSNotFound) {
        title += " (\(library.estimatedAssetCount))"
      }
      cell.textLabel!.text = title
    case 1:
      let album = userAlbums[indexPath.row] as! PHAssetCollection
      var title = album.localizedTitle!
      if (album.estimatedAssetCount != NSNotFound) {
        title += " (\(album.estimatedAssetCount))"
      }
      cell.textLabel!.text = title
    default:
      break
    }
    
    cell.accessoryType = .disclosureIndicator
    
    return cell
  }

}

// MARK: - AssetPickerDelegate

extension AlbumsTableViewController: AssetPickerDelegate {
  func assetPickerDidCancel() {
    assetPickerDelegate?.assetPickerDidCancel()
  }
  
  func assetPickerDidFinishPickingAssets(_ selectedAssets: [PHAsset])  {
    assetPickerDelegate?.assetPickerDidFinishPickingAssets(selectedAssets)
    // Clear out selections
    self.selectedAssets?.assets.removeAll()
  }
}
8. AssetsCollectionViewController.swift
import UIKit
import Photos

private let reuseIdentifier = "AssetCell"

class AssetsCollectionViewController: UICollectionViewController {
  // MARK: - Variables
  var assetsFetchResults: PHFetchResult<AnyObject>?
  var selectedAssets: SelectedAssets!
  var assetPickerDelegate: AssetPickerDelegate?
  
  private var thumbnailSize = CGSize.zero
  private let imageManager = PHImageManager()
  
  private var doneButton: UIBarButtonItem!
  
  // MARK: - Lifecycle
  override func viewDidLoad() {
    super.viewDidLoad()
    
    collectionView!.allowsMultipleSelection = true
    
    doneButton = UIBarButtonItem(title: "Done", style: .done, target: self, action: #selector(donePressed(_:)))
  }
  
  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)
    
    collectionView!.reloadData()
    updateSelectedItems()
  }
  
  override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    collectionView!.reloadData()
    updateSelectedItems()
  }

  // MARK: - Button handlers
  @objc func donePressed(_ sender: UIBarButtonItem) {
    assetPickerDelegate?.assetPickerDidFinishPickingAssets(selectedAssets.assets)
  }
  
}

// MARK: - Private Methods

extension AssetsCollectionViewController {
  private func updateSelectedItems() {
    guard selectedAssets != nil else { return }
    // Select the selected items
    if let fetchResult = assetsFetchResults {
      for asset in selectedAssets.assets {
        let index = fetchResult.index(of: asset)
        if index != NSNotFound {
          let indexPath = IndexPath(item: index, section: 0)
          collectionView!.selectItem(at: indexPath,
                                     animated: false, scrollPosition: UICollectionView.ScrollPosition())
        }
      }
    } else {
      for i in 0..<selectedAssets.assets.count {
        let indexPath = IndexPath(item: i, section: 0)
        collectionView!.selectItem(at: indexPath,
                                   animated: false, scrollPosition: UICollectionView.ScrollPosition())
      }
    }
    updateDoneButton()
  }
  
  private func updateDoneButton() {
    guard selectedAssets != nil else { return }
    // Add a done button when there are selected assets
    if selectedAssets.assets.count > 0 {
      navigationItem.rightBarButtonItem = doneButton
    } else {
      navigationItem.rightBarButtonItem = nil
    }
  }
}

// MARK: - UICollectionViewDataSource

extension AssetsCollectionViewController {
  override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
    return assetsFetchResults?.count ?? 0
  }
  
  override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! AssetCollectionViewCell
    
    // Configure the cell
    if let fetchResults = assetsFetchResults {
      let asset = fetchResults[indexPath.item] as! PHAsset
      let options = PHImageRequestOptions()
      options.isNetworkAccessAllowed = true
      
      cell.representedAssetIdentifier = asset.localIdentifier
      imageManager.requestImage(for: asset,
                                targetSize: thumbnailSize,
                                contentMode: .aspectFill,
                                options: options) { image, _ in
        if cell.representedAssetIdentifier == asset.localIdentifier {
          cell.thumbnailImage = image
        }
      }
    }
    
    return cell
  }
}

// MARK: - UICollectionViewDelegate

extension AssetsCollectionViewController {
  override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
    // Update selected Assets
    if let asset = assetsFetchResults?[indexPath.item] as? PHAsset {
      selectedAssets.assets.append(asset)
      updateDoneButton()
    }
  }
  
  override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
    // Update de-selected Assets
    if let assetToDelete = assetsFetchResults?[indexPath.item] as? PHAsset {
      selectedAssets.assets = selectedAssets.assets.filter { asset in
        !(asset == assetToDelete)
      }
      if assetsFetchResults == nil {
        collectionView.deleteItems(at: [indexPath])
      }
      updateDoneButton()
    }
  }
}
9. AssetCollectionViewCell.swift
import UIKit

class AssetCollectionViewCell: UICollectionViewCell {
  @IBOutlet var imageView: UIImageView!
  @IBOutlet private var checkMark: UIView?
  
  var representedAssetIdentifier: String!
  var thumbnailImage: UIImage! {
    didSet {
      imageView.image = thumbnailImage
    }
  }
  
  override var isSelected: Bool {
    didSet {
      checkMark?.isHidden = !isSelected
    }
  }
  
  override func prepareForReuse() {
    super.prepareForReuse()
    imageView.image = nil
  }
}
10. CheckMark.swift
import UIKit
import CoreGraphics

class CheckMark: UIView {
  
  override func draw(_ rect: CGRect) {
    super.draw(rect)
    drawRectChecked()
  }
  
  func drawRectChecked() {
    // General Declarations
    let context = UIGraphicsGetCurrentContext()
    
    // Color Declarations
    let checkmarkBlue2 = UIColor(red: 0.078, green: 0.435, blue: 0.875, alpha: 1.0)
    
    // Shadow Declarations
    let shadow2 = UIColor.black
    let shadow2Offset = CGSize(width: 0.1, height: -0.1)
    let shadow2BlurRadius = 2.5
    
    // Frames
    let frame = bounds
    
    // Subframes
    let group = CGRect(x: frame.minX + 3, y: frame.minY + 3, width: frame.width - 6, height: frame.height - 6)
    
    // CheckedOval Drawing
    let checkedOvalPath = UIBezierPath(ovalIn:CGRect(x: group.minX + 0.5, y: group.minY + 0.5, width: group.width + 1, height: group.height + 1))
    context?.saveGState()
    context?.setShadow(offset: shadow2Offset, blur: CGFloat(shadow2BlurRadius), color: shadow2.cgColor)
    checkmarkBlue2.setFill()
    checkedOvalPath.fill()
    context?.restoreGState()
    
    UIColor.white.setStroke()
    checkedOvalPath.lineWidth = 1
    checkedOvalPath.stroke()
    
    
    // Bezier Drawing
    let bezierPath = UIBezierPath()
    bezierPath.move(to: CGPoint(x: group.minX + 0.27083 * group.width, y: group.minY + 0.54167 * group.height))
    bezierPath.addLine(to: CGPoint(x: group.minX + 0.41667 * group.width, y: group.minY + 0.68750 * group.height))
    bezierPath.addLine(to: CGPoint(x: group.minX + 0.75000 * group.width, y: group.minY + 0.35417 * group.height))
    bezierPath.lineCapStyle = .square
    
    UIColor.white.setStroke()
    bezierPath.lineWidth = 1.3
    bezierPath.stroke()
  }
}
11. SelectedAssets.swift
import Foundation
import Photos

class SelectedAssets: NSObject {
  var assets: [PHAsset]
  
  override init() {
    assets = []
  }
  
  init(assets:[PHAsset]) {
    self.assets = assets
  }
}
12. UIImage+Alpha.swift
import UIKit

extension UIImage {
  
  var hasAlpha: Bool {
    guard let cgImage = self.cgImage else { return false }
    let alpha = cgImage.alphaInfo
    return alpha == .first || alpha == .last || alpha == .premultipliedFirst || alpha == .premultipliedLast
  }
  
  func imageWithAlpha() -> UIImage {
    if hasAlpha { return self }
    
    let scale = max(self.scale, 1)
    guard let imageRef = self.cgImage, let colorSpace = imageRef.colorSpace else { return self }
    let width = imageRef.width * Int(scale)
    let height = imageRef.height * Int(scale)
    
    guard let offscreenContext = CGContext(data: nil, width: width, height: height, bitsPerComponent: 8, bytesPerRow: 0, space: colorSpace, bitmapInfo: UInt32(CGBitmapInfo.alphaInfoMask.rawValue) & UInt32(CGImageAlphaInfo.premultipliedLast.rawValue)) else {
      return self
    }
    
    offscreenContext.draw(imageRef, in: CGRect(x: 0, y: 0, width: width, height: height))
    guard let imageRefWithAlpha = offscreenContext.makeImage() else { return self }
    return UIImage(cgImage: imageRefWithAlpha, scale: self.scale, orientation: .up)
  }
  
  func transparentBorderImage(borderSize: CGFloat) -> UIImage {
    let image = imageWithAlpha()
    let scale = max(self.scale, 1)
    let scaledBorderSize = borderSize * scale
    let newRect = CGRect(x: 0, y: 0, width: image.size.width * scale + scaledBorderSize * 2, height: image.size.height * scale + scaledBorderSize * 2)
    guard let imageRef = self.cgImage, let colorSpace = imageRef.colorSpace else {
      return self
    }
    guard let bitmap = CGContext(data: nil, width: Int(newRect.size.width), height: Int(newRect.size.height), bitsPerComponent: imageRef.bitsPerComponent, bytesPerRow: imageRef.bytesPerRow, space: colorSpace, bitmapInfo: imageRef.bitmapInfo.rawValue) else {
      return self
    }
    
    let imageLocation = CGRect(x: scaledBorderSize, y: scaledBorderSize, width: image.size.width * scale, height: image.size.height * scale)
    bitmap.draw(imageRef, in: imageLocation)
    guard let borderImageRef = bitmap.makeImage() else {
      return self
    }
    
    let maskImageRef = newBorderMask(borderSize: scaledBorderSize, size: newRect.size)
    guard let transparentBorderImageRef = borderImageRef.masking(maskImageRef) else {
      return self
    }
    return UIImage(cgImage: transparentBorderImageRef, scale: self.scale, orientation: .up)
  }
  
  func newBorderMask(borderSize: CGFloat, size: CGSize) -> CGImage {
    let colorSpace = CGColorSpaceCreateDeviceGray()
    
    let maskContext = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: 8, space: colorSpace, bitmapInfo: UInt32(CGImageAlphaInfo.none.rawValue))!
    
    maskContext.setFillColor(UIColor.black.cgColor)
    maskContext.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height))
    maskContext.setFillColor(UIColor.white.cgColor)
    maskContext.fill(CGRect(x: borderSize, y: borderSize, width: size.width - borderSize * 2, height: size.height - borderSize * 2))
    
    return maskContext.makeImage()!
  }
  
}
13. UIImage+Resize.swift
import UIKit

extension UIImage {
  
  func croppedImage(bounds: CGRect) -> UIImage {
    let scale = max(self.scale, 1)
    let scaledBounds = CGRect(x: bounds.origin.x * scale, y: bounds.origin.y * scale, width: bounds.size.width * scale, height: bounds.size.height * scale)
    guard let imageRef = cgImage?.cropping(to: scaledBounds) else {
      return self
    }
    let croppedImage = UIImage(cgImage: imageRef, scale: self.scale, orientation: .up)
    return croppedImage
  }
  
  func thumbnailImage(_ size: Int, transparentBorder borderSize: CGFloat, cornerRadius: CGFloat, interpolationQuality quality: CGInterpolationQuality) -> UIImage {
    let resizedImage = self.resizedImage(withContentMode: .scaleAspectFill, bounds: CGSize(width: size, height: size), interpolationQuality: quality)
    let cropRect = CGRect(x: round((resizedImage.size.width - CGFloat(size)) / 2), y: round((resizedImage.size.height - CGFloat(size)) / 2), width: CGFloat(size), height: CGFloat(size))
    let croppedImage = resizedImage.croppedImage(bounds: cropRect)
    let transparentBorderImage = borderSize != 0 ? croppedImage.transparentBorderImage(borderSize: borderSize) : croppedImage
    return transparentBorderImage.roundedCornerImage(cornerSize: cornerRadius, borderSize: borderSize)
  }
  
  func resizedImage(newSize: CGSize, interpolationQuality quality: CGInterpolationQuality) -> UIImage {
    let drawTransposed: Bool
    switch imageOrientation {
    case .left, .leftMirrored, .right, .rightMirrored:
      drawTransposed = true
    default:
      drawTransposed = false
    }
    let transform = transformForOrientation(newSize: newSize)
    return resizedImage(newSize: newSize, transform: transform, drawTransposed: drawTransposed, interpolationQuality: quality)
  }
  
  func resizedImage(withContentMode contentMode: UIView.ContentMode, bounds: CGSize, interpolationQuality quality: CGInterpolationQuality) -> UIImage {
    let horizontalRatio = bounds.width / size.width
    let verticalRatio = bounds.height / size.height
    let ratio: CGFloat
    switch contentMode {
    case .scaleAspectFill:
      ratio = max(horizontalRatio, verticalRatio)
    case .scaleAspectFit:
      ratio = min(horizontalRatio, verticalRatio)
    default:
      fatalError("Unsupported content mode: \(contentMode)")
    }
    let newSize = CGSize(width: size.width * ratio, height: size.height * ratio)
    return resizedImage(newSize: newSize, interpolationQuality: quality)
  }
  
  func resizedImage(newSize: CGSize, transform: CGAffineTransform, drawTransposed: Bool, interpolationQuality quality: CGInterpolationQuality) -> UIImage {
    let scale = max(self.scale, 1)
    let newRect = CGRect(x: 0, y: 0, width: newSize.width * scale, height: newSize.height * scale).integral
    let transposedRect = CGRect(x: 0, y: 0, width: newRect.size.height, height: newRect.size.width)
    guard let imageRef = self.cgImage else {
      return self
    }
    
    let colorSpace = CGColorSpaceCreateDeviceRGB()
    guard let bitmap = CGContext(data: nil, width: Int(newRect.size.width), height: Int(newRect.size.height), bitsPerComponent: 8, bytesPerRow: Int(newRect.size.width) * 4, space: colorSpace, bitmapInfo: UInt32(CGBitmapInfo.alphaInfoMask.rawValue) & UInt32(CGImageAlphaInfo.premultipliedLast.rawValue)) else {
      return self
    }
    bitmap.concatenate(transform)
    bitmap.interpolationQuality = quality
    bitmap.draw(imageRef, in: drawTransposed ? transposedRect : newRect)
    
    guard let newImageRef = bitmap.makeImage() else {
      return self
    }
    return UIImage(cgImage: newImageRef, scale: self.scale, orientation: .up)
  }
  
  func transformForOrientation(newSize: CGSize) -> CGAffineTransform {
    var transform = CGAffineTransform.identity
    
    switch imageOrientation {
    case .down, .downMirrored:
      transform = transform.translatedBy(x: newSize.width, y: newSize.height)
      transform = transform.rotated(by: CGFloat.pi)
    case .left, .leftMirrored:
      transform = transform.translatedBy(x: newSize.width, y: 0)
      transform = transform.rotated(by: CGFloat.pi / 2)
    case .right, .rightMirrored:
      transform = transform.translatedBy(x: 0, y: newSize.height)
      transform = transform.rotated(by: -CGFloat.pi / 2)
    default:
      break
    }
    
    switch imageOrientation {
    case .upMirrored, .downMirrored:
      transform = transform.translatedBy(x: newSize.width, y: 0)
      transform = transform.scaledBy(x: -1, y: 1)
    case .leftMirrored, .rightMirrored:
      transform = transform.translatedBy(x: newSize.height, y: 0)
      transform = transform.scaledBy(x: -1, y: 1)
    default:
      break
    }
    
    return transform
  }
  
}
14. UIImage+RoundedCorner.swift
import UIKit

extension UIImage {
  
  func roundedCornerImage(cornerSize: CGFloat, borderSize: CGFloat) -> UIImage {
    let image = imageWithAlpha()
    let scale = max(self.scale, 1.0)
    let scaledBorderSize = CGFloat(borderSize) * scale
    
    guard let imageRef = cgImage, let colorSpace = imageRef.colorSpace else {
      return self
    }
    
    guard let context = CGContext(data: nil, width: Int(image.size.width * scale), height: Int(image.size.height * scale), bitsPerComponent: imageRef.bitsPerComponent, bytesPerRow: 0, space: colorSpace, bitmapInfo: imageRef.bitmapInfo.rawValue) else {
      return self
    }
    
    context.beginPath()
    addRoundedRectToPath(rect: CGRect(x: scaledBorderSize, y: scaledBorderSize, width: image.size.width * scale, height: image.size.height * scale), context: context, ovalWidth: cornerSize * scale, ovalHeight: cornerSize * scale)
    context.closePath()
    context.clip()
    
    context.draw(imageRef, in: CGRect(x: 0, y: 0, width: image.size.width * scale, height: image.size.height * scale))
    guard let clippedImage = context.makeImage() else {
      return self
    }
    
    return UIImage(cgImage: clippedImage, scale: self.scale, orientation: .up)
  }
  
  func addRoundedRectToPath(rect: CGRect, context: CGContext, ovalWidth: CGFloat, ovalHeight: CGFloat) {
    if ovalWidth == 0 || ovalHeight == 0 {
      context.addRect(rect)
      return
    }
    context.saveGState()
    context.translateBy(x: rect.minX, y: rect.minY)
    context.scaleBy(x: ovalWidth, y: ovalHeight)
    let fw = rect.width / ovalWidth
    let fh = rect.height / ovalHeight
    context.move(to: CGPoint(x: fw, y: fh / 2))
    context.addArc(tangent1End: CGPoint(x: fw, y: fh), tangent2End: CGPoint(x: fw / 2, y: fh), radius: 1)
    context.addArc(tangent1End: CGPoint(x: 0, y: fh), tangent2End: CGPoint(x: 0, y: fh / 2), radius: 1)
    context.addArc(tangent1End: CGPoint(x: 0, y: 0), tangent2End: CGPoint(x: fw / 2, y: 0), radius: 1)
    context.addArc(tangent1End: CGPoint(x: fw, y: 0), tangent2End: CGPoint(x: fw, y: fh / 2), radius: 1)
    context.closePath()
    context.restoreGState()
  }
  
}

后记

本篇主要讲述了GCD之一个简单应用示例源码,感兴趣的给个赞或者关注~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容