美文网首页
PhotoKit框架详细解析(三) —— 图像的获取、修改、保存

PhotoKit框架详细解析(三) —— 图像的获取、修改、保存

作者: 刀客传奇 | 来源:发表于2020-10-09 06:30 被阅读0次

版本记录

版本号 时间
V1.0 2020.10.09 星期五

前言

在我们开发中总有和系统相册进行交互的时候,包含图片和视频的获取,存储,修改等操作。这个模块我们就一起来看下这个相关的框架PhotoKit。感兴趣的可以看下面几篇文章。
1. PhotoKit框架详细解析(一) —— 基本概览(一)
2. PhotoKit框架详细解析(二) —— 图像的获取、修改、保存、编辑以及撤销等简单示例(一)

源码

1. Swift

首先我们看下工程组织结构

接着看下sb中的内容

下面就是源码了

1. AlbumCollectionViewController.swift
import UIKit
import Photos

class AlbumCollectionViewController: UICollectionViewController {
  var sections: [AlbumCollectionSectionType] = [.all, .smartAlbums, .userCollections]
  var allPhotos = PHFetchResult<PHAsset>()
  var smartAlbums = PHFetchResult<PHAssetCollection>()
  var userCollections = PHFetchResult<PHAssetCollection>()

  override func viewDidLoad() {
    super.viewDidLoad()
    getPermissionIfNecessary { granted in
      guard granted else { return }
      self.fetchAssets()
      DispatchQueue.main.async {
        self.collectionView.reloadData()
      }
    }
    PHPhotoLibrary.shared().register(self)
  }

  deinit {
    PHPhotoLibrary.shared().unregisterChangeObserver(self)
  }

  @IBSegueAction func makePhotosCollectionViewController(_ coder: NSCoder) -> PhotosCollectionViewController? {
    guard
      let selectedIndexPath = collectionView.indexPathsForSelectedItems?.first
      else { return nil }

    let sectionType = sections[selectedIndexPath.section]
    let item = selectedIndexPath.item

    let assets: PHFetchResult<PHAsset>
    let title: String

    switch sectionType {
    case .all:
      assets = allPhotos
      title = AlbumCollectionSectionType.all.description
    case .smartAlbums, .userCollections:
      let album =
        sectionType == .smartAlbums ? smartAlbums[item] : userCollections[item]
      assets = PHAsset.fetchAssets(in: album, options: nil)
      title = album.localizedTitle ?? ""
    }

    return PhotosCollectionViewController(assets: assets, title: title, coder: coder)
  }

  override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
    if kind == UICollectionView.elementKindSectionHeader {
      guard let headerView = collectionView.dequeueReusableSupplementaryView(
        ofKind: kind,
        withReuseIdentifier: AlbumCollectionReusableView.reuseIdentifier,
        for: indexPath) as? AlbumCollectionReusableView
        else {
        fatalError("Unable to dequeue AlbumCollectionReusableView")
      }
      headerView.title.text = sections[indexPath.section].description
      return headerView
    }
    return UICollectionReusableView()
  }

  override func numberOfSections(in collectionView: UICollectionView) -> Int {
    return sections.count
  }

  override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    // 1
    guard let cell =
      collectionView.dequeueReusableCell(
        withReuseIdentifier: AlbumCollectionViewCell.reuseIdentifier,
        for: indexPath) as? AlbumCollectionViewCell
    else {
      fatalError("Unable to dequeue AlbumCollectionViewCell")
    }
    // 2
    var coverAsset: PHAsset?
    let sectionType = sections[indexPath.section]
    switch sectionType {
    // 3
    case .all:
      coverAsset = allPhotos.firstObject
      cell.update(title: sectionType.description, count: allPhotos.count)
    // 4
    case .smartAlbums, .userCollections:
      let collection = sectionType == .smartAlbums ?
        smartAlbums[indexPath.item] :
        userCollections[indexPath.item]
      let fetchedAssets = PHAsset.fetchAssets(in: collection, options: nil)
      coverAsset = fetchedAssets.firstObject
      cell.update(title: collection.localizedTitle, count: fetchedAssets.count)
    }
    // 5
    guard let asset = coverAsset else { return cell }
    cell.photoView.fetchImageAsset(asset, targetSize: cell.bounds.size) { success in
      cell.photoView.isHidden = !success
      cell.emptyView.isHidden = success
    }
    return cell
  }

  func getPermissionIfNecessary(completionHandler: @escaping (Bool) -> Void) {
    // 1
    guard PHPhotoLibrary.authorizationStatus() != .authorized else {
      completionHandler(true)
      return
    }
    // 2
    PHPhotoLibrary.requestAuthorization { status in
      completionHandler(status == .authorized ? true : false)
    }
  }

  func fetchAssets() {// 1
    let allPhotosOptions = PHFetchOptions()
    allPhotosOptions.sortDescriptors = [
      NSSortDescriptor(
        key: "creationDate",
        ascending: false)
    ]
    // 2
    allPhotos = PHAsset.fetchAssets(with: allPhotosOptions)
    // 3
    smartAlbums = PHAssetCollection.fetchAssetCollections(
      with: .smartAlbum,
      subtype: .albumRegular,
      options: nil)
    // 4
    userCollections = PHAssetCollection.fetchAssetCollections(
      with: .album,
      subtype: .albumRegular,
      options: nil)
  }

  override func collectionView(
    _ collectionView: UICollectionView,
    numberOfItemsInSection section: Int
  ) -> Int {
    switch sections[section] {
    case .all: return 1
    case .smartAlbums: return smartAlbums.count
    case .userCollections: return userCollections.count
    }
  }
}

extension AlbumCollectionViewController: PHPhotoLibraryChangeObserver {
  func photoLibraryDidChange(_ changeInstance: PHChange) {
    DispatchQueue.main.sync {
      // 1
      if let changeDetails = changeInstance.changeDetails(for: allPhotos) {
        allPhotos = changeDetails.fetchResultAfterChanges
      }
      // 2
      if let changeDetails = changeInstance.changeDetails(for: smartAlbums) {
        smartAlbums = changeDetails.fetchResultAfterChanges
      }
      if let changeDetails = changeInstance.changeDetails(for: userCollections) {
        userCollections = changeDetails.fetchResultAfterChanges
      }
      // 4
      collectionView.reloadData()
    }
  }
}
2. PhotosCollectionViewController.swift
import UIKit
import Photos

class PhotosCollectionViewController: UICollectionViewController {
  var assets: PHFetchResult<PHAsset>

  required init?(coder: NSCoder) {
    fatalError("init(coder:) not implemented.")
  }

  init?(assets: PHFetchResult<PHAsset>, title: String, coder: NSCoder) {
    self.assets = assets
    super.init(coder: coder)
    self.title = title
  }

  override func viewDidLoad() {
    super.viewDidLoad()
    PHPhotoLibrary.shared().register(self)
  }

  deinit {
    PHPhotoLibrary.shared().unregisterChangeObserver(self)
  }

  @IBSegueAction func makePhotoViewController(_ coder: NSCoder) -> PhotoViewController? {
    guard let selectedIndexPath = collectionView.indexPathsForSelectedItems?.first else { return nil }
    return PhotoViewController(asset: assets[selectedIndexPath.item], coder: coder)
  }

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

  override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
    guard let cell = collectionView.dequeueReusableCell(
      withReuseIdentifier: PhotoCollectionViewCell.reuseIdentifier,
      for: indexPath) as? PhotoCollectionViewCell else {
        fatalError("Unable to dequeue PhotoCollectionViewCell")
    }
    let asset = assets[indexPath.item]
    cell.photoView.fetchImageAsset(asset, targetSize: cell.photoView.bounds.size, completionHandler: nil)
    return cell
  }
}

extension PhotosCollectionViewController: PHPhotoLibraryChangeObserver {
  func photoLibraryDidChange(_ changeInstance: PHChange) {
    // 1
    guard let change = changeInstance.changeDetails(for: assets) else {
      return
    }
    DispatchQueue.main.sync {
      // 2
      assets = change.fetchResultAfterChanges
      collectionView.reloadData()
    }
  }
}
3. PhotoViewController.swift
import UIKit
import Photos
import PhotosUI

class PhotoViewController: UIViewController {
  @IBOutlet weak var imageView: UIImageView!

  @IBOutlet weak var toolbar: UIToolbar!

  @IBOutlet weak var favoriteButton: UIBarButtonItem!
  @IBAction func favoriteTapped(_ sender: Any) { toggleFavorite() }

  @IBOutlet weak var saveButton: UIBarButtonItem!
  @IBAction func saveTapped(_ sender: Any) { saveImage() }

  @IBOutlet weak var undoButton: UIBarButtonItem!
  @IBAction func undoTapped(_ sender: Any) { undo() }

  @IBAction func applyFilterTapped(_ sender: Any) { applyFilter() }

  var asset: PHAsset
  var editingOutput: PHContentEditingOutput?

  required init?(coder: NSCoder) {
    fatalError("init(coder:) not implemented")
  }

  init?(asset: PHAsset, coder: NSCoder) {
    self.asset = asset
    super.init(coder: coder)
  }

  override func viewDidLoad() {
    super.viewDidLoad()
    getPhoto()
    updateFavoriteButton()
    updateUndoButton()
    saveButton.isEnabled = false
    PHPhotoLibrary.shared().register(self)
  }

  deinit {
    PHPhotoLibrary.shared().unregisterChangeObserver(self)
  }

  func updateFavoriteButton() {
    if asset.isFavorite {
      favoriteButton.image = UIImage(systemName: "heart.fill")
    } else {
      favoriteButton.image = UIImage(systemName: "heart")
    }
  }

  func updateUndoButton() {
    let adjustmentResources = PHAssetResource.assetResources(for: asset)
      .filter { $0.type == .adjustmentData }
    undoButton.isEnabled = !adjustmentResources.isEmpty
  }

  func toggleFavorite() {
    // 1
    let changeHandler: () -> Void = {
      let request = PHAssetChangeRequest(for: self.asset)
      request.isFavorite = !self.asset.isFavorite
    }
    // 2
    PHPhotoLibrary.shared().performChanges(changeHandler, completionHandler: nil)
  }

  func applyFilter() {
    // 1
    asset.requestContentEditingInput(with: nil) { input, _ in
      // 2
      guard let bundleID = Bundle.main.bundleIdentifier else {
        fatalError("Error: unable to get bundle identifier")
      }
      guard let input = input else {
        fatalError("Error: cannot get editing input")
      }
      guard let filterData = Filter.noir.data else {
        fatalError("Error: cannot get filter data")
      }
      // 3
      let adjustmentData = PHAdjustmentData(
        formatIdentifier: bundleID,
        formatVersion: "1.0",
        data: filterData)
      // 4
      self.editingOutput = PHContentEditingOutput(contentEditingInput: input)
      guard let editingOutput = self.editingOutput else { return }
      editingOutput.adjustmentData = adjustmentData
      // 5
      let fitleredImage = self.imageView.image?.applyFilter(.noir)
      self.imageView.image = fitleredImage
      // 6
      let jpegData = fitleredImage?.jpegData(compressionQuality: 1.0)
      do {
        try jpegData?.write(to: editingOutput.renderedContentURL)
      } catch {
        print(error.localizedDescription)
      }
      // 7
      DispatchQueue.main.async {
        self.saveButton.isEnabled = true
      }
    }
  }

  func saveImage() {
    // 1
    let changeRequest: () -> Void = {
      let changeRequest = PHAssetChangeRequest(for: self.asset)
      changeRequest.contentEditingOutput = self.editingOutput
    }
    // 2
    let completionHandler: (Bool, Error?) -> Void = { success, error in
      guard success else {
        print("Error: cannot edit asset: \(String(describing: error))")
        return
      }
      // 3
      self.editingOutput = nil
      DispatchQueue.main.async {
        self.saveButton.isEnabled = false
      }
    }
    // 4
    PHPhotoLibrary.shared().performChanges(
      changeRequest,
      completionHandler: completionHandler)
  }

  func undo() {
    // 1
    let changeRequest: () -> Void = {
      let request = PHAssetChangeRequest(for: self.asset)
      request.revertAssetContentToOriginal()
    }
    // 2
    let completionHandler: (Bool, Error?) -> Void = { success, error in
      guard success else {
        print("Error: can't revert the asset: \(String(describing: error))")
        return
      }
      DispatchQueue.main.async {
        self.undoButton.isEnabled = false
      }
    }
    // 3
    PHPhotoLibrary.shared().performChanges(
      changeRequest,
      completionHandler: completionHandler)
  }

  func getPhoto() {
    imageView.fetchImageAsset(asset, targetSize: view.bounds.size, completionHandler: nil)
  }
}

// 1
extension PhotoViewController: PHPhotoLibraryChangeObserver {
  func photoLibraryDidChange(_ changeInstance: PHChange) {
    // 2
    guard
      let change = changeInstance.changeDetails(for: asset),
      let updatedAsset = change.objectAfterChanges
      else { return }
    // 3
    DispatchQueue.main.sync {
      // 4
      asset = updatedAsset
      imageView.fetchImageAsset(
        asset,
        targetSize: view.bounds.size
      ) { [weak self] _ in
        guard let self = self else { return }
        // 5
        self.updateFavoriteButton()
        self.updateUndoButton()
      }
    }
  }
}
4. UIImageView+Extension.swift
import UIKit
import Photos

extension UIImageView {
  func fetchImageAsset(_ asset: PHAsset?, targetSize size: CGSize, contentMode: PHImageContentMode = .aspectFill, options: PHImageRequestOptions? = nil, completionHandler: ((Bool) -> Void)?) {
    // 1
    guard let asset = asset else {
      completionHandler?(false)
      return
    }
    // 2
    let resultHandler: (UIImage?, [AnyHashable: Any]?) -> Void = { image, info in
      self.image = image
      completionHandler?(true)
    }
    // 3
    PHImageManager.default().requestImage(
      for: asset,
      targetSize: size,
      contentMode: contentMode,
      options: options,
      resultHandler: resultHandler)
  }
}
5. AlbumCollectionReusableView.swift
import UIKit

class AlbumCollectionReusableView: UICollectionReusableView {
  static let reuseIdentifier = "headerView"
  @IBOutlet weak var title: UILabel!
}
6. AlbumCollectionViewCell.swift
import UIKit

class AlbumCollectionViewCell: UICollectionViewCell {
  static let reuseIdentifier = "albumCell"
  @IBOutlet weak var emptyView: UIImageView!
  @IBOutlet weak var photoView: UIImageView!
  @IBOutlet weak var albumTitle: UILabel!
  @IBOutlet weak var albumCount: UILabel!

  override func prepareForReuse() {
    super.prepareForReuse()
    albumTitle.text = "Untitled"
    albumCount.text = "0 photos"
    photoView.image = nil
    photoView.isHidden = true
    emptyView.isHidden = false
  }

  func update(title: String?, count: Int) {
    albumTitle.text = title ?? "Untitled"
    albumCount.text = "\(count.description) \(count == 1 ? "photo" : "photos")"
  }
}
7. AlbumSectionType.swift
enum AlbumCollectionSectionType: Int, CustomStringConvertible {
  case all, smartAlbums, userCollections

  var description: String {
    switch self {
    case .all: return "All Photos"
    case .smartAlbums: return "Smart Albums"
    case .userCollections: return "User Collections"
    }
  }
}
8. AlbumCollectionViewController+Extensions.swift
import UIKit

extension AlbumCollectionViewController: UICollectionViewDelegateFlowLayout {
  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CollectionViewFlowLayoutType(.album, frame: view.frame).sizeForItem
  }

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    return CollectionViewFlowLayoutType(.album, frame: view.frame).sectionInsets
  }

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
    return CollectionViewFlowLayoutType(.album, frame: view.frame).sectionInsets.left
  }
}
9. CollectionViewFlowLayout.swift
import UIKit

struct CollectionViewFlowLayoutType {
  enum ViewType { case album, photos }

  private var viewType: ViewType = .album
  private var viewFrame: CGRect = .zero
  var itemsPerRow: CGFloat {
    switch viewType {
    case .album: return 2
    case .photos: return 3
    }
  }
  var sectionInsets: UIEdgeInsets {
    switch viewType {
    case .album: return UIEdgeInsets(top: 4.0, left: 8.0, bottom: 4.0, right: 8.0)
    case .photos: return UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0)
    }
  }
  var sizeForItem: CGSize {
    let paddingSpace = sectionInsets.left * (itemsPerRow + 1)
    let availableWidth = viewFrame.width - paddingSpace
    let widthPerItem = availableWidth / itemsPerRow
    return CGSize(width: widthPerItem, height: widthPerItem)
  }

  init(_ type: ViewType, frame: CGRect) {
    viewType = type
    viewFrame = frame
  }
}
10. Filter.swift 
import Foundation

enum Filter: String {
  case noir = "CIPhotoEffectNoir"

  var data: Data? {
    return self.rawValue.data(using: .utf8)
  }
}
11. PhotoCollectionViewCell.swift
import UIKit

class PhotoCollectionViewCell: UICollectionViewCell {
  static let reuseIdentifier = "photoCell"
  @IBOutlet weak var photoView: UIImageView!
  @IBOutlet weak var livePhotoIndicator: UIImageView!

  override func prepareForReuse() {
    super.prepareForReuse()
    photoView.image = nil
    livePhotoIndicator.isHidden = true
  }
}
12. PhotosCollectionViewController+Extensions.swift
import UIKit

extension PhotosCollectionViewController: UICollectionViewDelegateFlowLayout {
  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    return CollectionViewFlowLayoutType(.photos, frame: view.frame).sizeForItem
  }

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
    return CollectionViewFlowLayoutType(.photos, frame: view.frame).sectionInsets
  }

  func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
    return CollectionViewFlowLayoutType(.photos, frame: view.frame).sectionInsets.left
  }
}
13. UIImage+Extensions.swift
import UIKit

extension UIImage {
  func applyFilter(_ filter: Filter) -> UIImage? {
    let filter = CIFilter(name: filter.rawValue)
    let inputImage = CIImage(image: self)
    filter?.setValue(inputImage, forKey: "inputImage")
    guard let finalImage = filter?.outputImage else { return nil }
    guard let cgImage = CIContext().createCGImage(finalImage, from: finalImage.extent) else { return nil }
    return UIImage(cgImage: cgImage)
  }
}

后记

本篇主要讲述了图像的获取、修改、保存、编辑以及撤销等简单示例,感兴趣的给个赞或者关注~~~

相关文章

网友评论

      本文标题:PhotoKit框架详细解析(三) —— 图像的获取、修改、保存

      本文链接:https://www.haomeiwen.com/subject/mkkpuktx.html