美文网首页
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