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之一个简单应用示例源码,感兴趣的给个赞或者关注~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容