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

2020-09-30  本文已影响0人  刀客传奇

版本记录

版本号 时间
V1.0 2020.09.30 星期三

前言

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

开始

首先看下主要内容:

在本教程中,您将学习如何使用PhotoKit访问和修改照片,智能相册和用户收藏。 您还将学习如何保存和还原对照片所做的修改。本片内容来自翻译

下面看一下写作环境

Swift 5, iOS 13, Xcode 11

Photos应用通过一组称为PhotoKitAPI在iOS中管理图像资源。 如果您一直想知道如何构建像Photos之类的应用程序,或者只是访问照片库,PhotoKit就是答案。 本教程将重点放在iOS上,但PhotoKit也可用于macOSCatalysttvOS

您将使用NoirIt,这是一款可将精美的Noir滤镜应用于照片的应用程序。 为此,您将:

下载入门项目。

首先打开启动文件夹中的NoirIt.xcodeproj。 展开资源文件夹,然后打开Main.storyboard

该应用程序的布局非常简单。 有一个相册收集视图控制器,一个照片收集视图控制器和一个照片细节视图控制器。

构建并运行。

现在看可能不多,但是很快。

1. Prepping the Photos App

在开始之前,请在Photos中创建一个相册,以便以后至少可以在NoirIt中查看一个相册。

这就是您需要在Photos中执行的所有操作。


Getting PhotoKit Permissions

与许多iOS API一样,PhotoKit使用权限模型。 它向用户显示一个对话框,询问该应用访问其图像的权限。 在深入访问和修改图像之前,必须先获得许可。 您可以使用PHPhotoLibrary(共享对象来管理对照片库的访问)来执行此操作。

1. Modifying Info.plist

第一步是向Info.plist添加一个密钥,该密钥描述为什么要获得访问该库的权限。

您的Info.plist应该如下所示:

2. Requesting Authorization

打开AlbumCollectionViewController.swift。 找到getPermissionIfNecessary(completionHandler :)并将其实现替换为:

// 1
guard PHPhotoLibrary.authorizationStatus() != .authorized else {
  completionHandler(true)
  return
}
// 2
PHPhotoLibrary.requestAuthorization { status in
  completionHandler(status == .authorized)
}

注意:PHAuthorizationStatus是一个枚举,它也可以返回notDefinededrestricteddeniediOS 14的新增内容limited。 您可能需要检查并适当处理它们。 现在,让NoirIt保持简单。

viewDidLoad()已经在调用此方法,因此进行构建并运行。 当NoirIt启动时,iOS会请求访问照片库的权限。 如果您使用的是iOS 13,请点击OK,或者在iOS 14上,点击Allow Access to All Photos


Understanding Assets

即使您最终会获得图像,也必须了解您主要使用PhotoKit中的资源。 考虑一下您如何与Photos应用进行交互。 当然,您可以查看图像,但其中也包含元数据,例如收藏夹和地理编码的位置数据。 不仅限于图像。 Photos包含LivePhotos和视频。 将这些东西塞进UIImage没有任何意义。 这就是PHAsset用到的地方。

PHAsset是描述图像,LivePhoto或视频的元数据。 它是不可变的,不包含图像本身,但确实提供了获取图像所需的信息。 它还包含大量信息,例如创建和修改日期,位置数据,收藏夹和隐藏状态,突发数据等等。 就像您很快就会看到的那样,PHAsset是真正的主力军。

有时您需要处理一组资源。 这些通常作为PHAssetCollection对象返回。

1. Asset Data Models

打开AlbumCollectionViewController.swift。 在文件顶部附近,在sections属性的声明下添加以下内容:

private var allPhotos = PHFetchResult<PHAsset>()
private var smartAlbums = PHFetchResult<PHAssetCollection>()
private var userCollections = PHFetchResult<PHAssetCollection>()

您可能会对自己说:“嘿,self,这些PHFetchResult是什么? 我以为我正在获取PHAssetsPHAssetCollections?” PHFetchResult的简化思考方式是将其视为一个数组,从本质上讲,它是一个数组。 它包含所有相同的数组方法和约定,例如count()index(of :)。 另外,它可以智能地处理数据的获取,缓存和根据需要重新获取。 如果您将PHFetchResult视为资源或集合的智能阵列,就可以了。 这些属性是应用程序的数据存储。

2. Fetching Assets and Asset Collections

仍在AlbumCollectionViewController.swift中,找到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)

现在填充了数据存储,下一个任务是更新UI。

3. Prepping the Collection View

您现在拥有资源,是时候对它们进行一些处理了。 在类末尾添加以下内容:

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
  }
}

在这里,您可以返回每个section中的items数,以便收集视图知道每个section中要显示多少个项目items。 除了“所有照片”部分,这是如何将PHFetchResult视为数组的一个很好的示例。

4. Updating the Cell

接下来,用以下代码替换collectionView(_:cellForItemAt :)中的代码:

// 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

构建并运行。现在,您将看到“所有照片”(All Photos)的条目,库中的每个智能相册以及每个用户集合。滚动到底部以查看您的My Cool Pics相册。

还不错,但是封面图像发生了什么? 接下来,您将解决此问题。


Fetching Images from Assets

该默认相册图像有点无聊。 最好能看到相册中的图像。

在上一步中,您调用了fetchImageAsset(_:targetSize:contentMode:options:completionHandler :)以获取资源的图像。 这是在扩展程序中添加到UIImage的自定义方法。 目前,它没有任何可提取图像的代码,并且始终返回false。 要解决此问题,您将使用PHImageManager。 图像管理器处理从资源中获取图像并缓存结果以便以后快速检索。

打开UIImageView + Extension.swift并将fetchImageAsset(_:targetSize:contentMode:options:completionHandler :)中的代码替换为:

// 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)

构建并运行。 您的相册现在有封面图像!

如果选择任何相册,则下一个视图为空。 您的下一个任务正在等待。


Displaying Album Assets

显示相册的所有资产仅需要从PHImageManager请求每个图像即可。 PhotosCollectionViewController已经设置为使用您刚刚使用的获取图像资产扩展来执行此操作。 要使此工作正常进行,您只需要设置segue即可传递获取结果。

AlbumCollectionViewController.swift中,找到makePhotosCollectionViewController(_ :)并将其代码替换为:

// 1
guard
  let selectedIndexPath = collectionView.indexPathsForSelectedItems?.first
  else { return nil }

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

// 3
let assets: PHFetchResult<PHAsset>
let title: String

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

// 6
return PhotosCollectionViewController(assets: assets, title: title, coder: coder)

现在:

构建并运行。 在相册视图中点击所有照片。 现在,您会看到所有照片的集合。

点击其中一个照片

事情正成为焦点!


Modifying Asset Metadata

1. Change Requests

修改资源的能力是NoirIt的关键组成部分。 通过允许用户将照片标记为收藏来进行资源修改。 PHAssetChangeRequest有助于资源的创建,修改和删除。

打开PhotoViewController.swift并将此代码添加到toggleFavorite()中:

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

接下来,用以下代码替换updateFavoriteButton()中的代码:

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

使用isFavorite属性检查PHAsset的收藏夹状态,然后将按钮图像设置为空心或实心。

构建并运行。 浏览该应用程序,然后选择您喜欢的照片。 轻按“收藏夹”按钮,然后…什么都没有发生。 那么出了什么问题?

2. Photo View Controller Change Observer

PhotoKit缓存获取请求的结果,以提高性能。 当您点击“收藏夹”按钮时,资源将在库中更新,但是视图控制器的资源副本现在已过期。 控制器需要侦听库的更新,并在必要时更新其资源。 通过使控制器符合PHPhotoLibraryChangeObserver来执行此操作。

在文件末尾的最后一个花括号之后,添加:

// 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()
      }
    }
  }
}

3. Registering the Photo View Controller

仍在PhotoViewController.swift中,找到viewDidLoad()并将其添加为最后一行:

PHPhotoLibrary.shared().register(self)

视图控制器必须注册才能接收更新。 在viewDidLoad()之后,添加:

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

完成后,视图控制器还必须注销。

构建并运行。 导航到您最喜欢的照片之一。 点击“心脏”按钮,心脏就会充满。 再次点击,它会还原。

但是有一个新问题。 再次点击“收藏夹”按钮,即可充满爱心。 导航回到All Photo视图,然后再次选择同一张照片。 心脏不再充满,并且如果您选择它,什么也不会发生。 有点不对劲。

4. Photos View Controller Change Observer

PhotosCollectionViewController也不符合PHPhotoLibraryChangeObserver。 因此,其资源也已过时。 修复非常简单:您需要使其符合PHPhotoLibraryChangeObserver

打开PhotosCollectionViewController.swift并滚动到文件末尾。 添加以下代码:

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()
    }
  }
}

这段代码与您在PhotoViewController中所做的相似,但有一些小区别:

5. Registering the Photos View Controller

滚动到viewDidLoad()并将其添加到super.viewDidLoad()之后:

PHPhotoLibrary.shared().register(self)

与上次一样,视图注册以接收库更新。 在viewDidLoad()之后添加:

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

该视图还需要注销。

6. Album View Controller Change Observer

在使用它时,应将类似的代码添加到AlbumCollectionViewController.swift。 如果您不这样做,则一路导航到最后都会遇到类似的问题。 打开AlbumCollectionViewController.swift并将以下内容添加到文件末尾:

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()
    }
  }
}

这段代码有些不同,因为您正在检查更改是否影响多个获取结果。

7. Album View Controller Registration

AlbumCollectionViewController.swift中添加代码以注册库更新到viewDidLoad()的末尾:

PHPhotoLibrary.shared().register(self)

viewDidLoad()后,添加:

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

同样,此视图也需要注销。

构建并运行。 点击All Photo,然后点击照片。 将其标记为收藏,然后一直导航回到主视图。 再次点击All Photo,然后点击同一张照片。 您会看到它仍被标记为收藏。 导航回到album collection view。 请注意,“收藏夹”相册计数是最新的,并且已为“收藏夹”设置了封面图像。

做得好! 现在,您持久化了对资源的元数据更改,并在每个视图控制器中显示这些更改。


Editing a Photo

打开PhotoViewController.swift并在声明asset属性后添加以下内容:

private var editingOutput: PHContentEditingOutput?

PHContentEditingOutput是一个容器,用于存储对资源的编辑。 稍后您将了解其工作原理。 找到applyFilter()并将以下代码添加到其中:

// 1
asset.requestContentEditingInput(with: nil) { [weak self] input, _ in
  guard let self = self else { return }

  // 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
  }
}

构建并运行。选择一张照片。点击Apply Filter。您的照片现在应该添加了一个漂亮的noir滤镜。

点击保存按钮。 没发生什么事。 接下来,您将对其进行修复。


Saving Edits

使用上面创建的编辑输出容器(editing output container)将更改保存到库。 同样,使用PHAssetChangeRequest就像您之前更改元数据一样。

仍然在PhotoViewController.swift中,找到saveImage()并添加以下内容:

// 1
let changeRequest: () -> Void = { [weak self] in
  guard let self = self else { return }
  let changeRequest = PHAssetChangeRequest(for: self.asset)
  changeRequest.contentEditingOutput = self.editingOutput
}
// 2
let completionHandler: (Bool, Error?) -> Void = { [weak self] success, error in
  guard let self = self else { return }

  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)

构建并运行。 导航到照片,然后点击Apply Filter按钮。 点击保存按钮。 iOS将显示一个对话框,询问修改照片的权限。 点击Modify

导航回到All Photos,然后再次选择照片。 您应该看到修改后的图像已成功保存。


Undoing Edits

照片视图控制器中只剩下一个无法使用的按钮:Undo。您现在可能已经知道:PHAssetChangeRequest

使用存在的资源更改数据来确定Undo按钮的启用状态。 查找updateUndoButton()并将其内容替换为:

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

对资源的每次编辑都会创建一个PHAssetResource对象。 assetResources(for :)返回给定资源的资源数组。 通过调整数据的存在来过滤资源。 如果进行编辑,则按钮的isEnabled属性设置为true,否则为false

现在该添加撤消逻辑了。 找到undo()并添加以下代码:

// 1
let changeRequest: () -> Void = { [weak self] in
  guard let self = self else { return }
  let request = PHAssetChangeRequest(for: self.asset)
  request.revertAssetContentToOriginal()
}
// 2
let completionHandler: (Bool, Error?) -> Void = { [weak self] success, error in
  guard let self = self else { return }

  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)

现在,这种模式应该已经很熟悉了。

构建并运行。 选择您对其应用滤镜的照片。 点击Undo。 就像您之前保存资源时一样,iOS会要求用户撤消所有更改的权限。

点击Revert。 图像变回原始图像。

PhotoKit提供了更多功能,例如LivePhoto,视频和照片编辑扩展。 请查看PhotoKit文档以获取更多信息:

后记

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

上一篇 下一篇

猜你喜欢

热点阅读