WWDC Quick Look 💓 By SwiftGGTeam
Discover PhotoKit change history

Discover PhotoKit change history

观看原视频

Highlight

之前要追踪照片库变化非常麻烦。新增资产需要按日期查询,修改需要比对 modification date(但不精确),删除更是要全量 diff。三种操作需要三种不同的检测方式,性能开销大,结果不可靠。


核心内容

一个照片类 App,常见需求是让自己的界面跟系统照片库保持同步。

Session 里的例子是一个登山社交 App。用户完成一次 hiking workout 后,App 按这次运动的开始和结束时间,从照片库里挑出相关照片,生成拼贴图。如果朋友后来发来新的登山照片,或者用户编辑了某张照片,拼贴图也应该更新。(00:54

旧做法要分三步。

新增照片,可以按 creationDate 查询。修改照片,要重新 fetch 已追踪的资产,再检查 modificationDate。删除照片最麻烦,要把所有已追踪资产重新取出,再跟本地记录做 diff。Apple 特别指出,modificationDate 还可能被照片库内部处理活动改动,导致误报。(01:25

PhotoKit 引入 persistent change history。它把照片库变化记录成一条时间线,包括插入、更新和删除。App 保存一个 persistent change token(持久变化令牌),下次启动时用这个 token 查询之后发生的变化。(02:42

这个 token 表示照片库在某个时间点的状态,可以跨 App 启动保存。它是设备本地的 token,访问成本低。如果 App 处在 limited library mode,只会返回用户已授权的 PhotoKit 对象变化。(03:17

Persistent history 补的是离线变化。原来的 PHChange 仍然适合 App 运行中、内存里 fetch result 的实时变化;persistent history 用来追踪 App 不活跃期间的照片库变化。两者可以按需求组合使用。(05:10


详细内容

旧做法:三类变化要三套检查

01:25)新增资产看起来简单:记录上次启动时间,查询 creationDate 晚于它的照片。

// Discover added assets
let options = PHFetchOptions()
options.predicate = NSPredicate(format: "creationDate > %@", lastLaunchDate as CVarArg)
let insertedAssets = PHAsset.fetchAssets(with: options)

关键点:

  • PHFetchOptions() 创建一次 PhotoKit 查询配置。
  • predicate 把结果限制在上次启动之后创建的资产。
  • PHAsset.fetchAssets(with:) 返回可能新增的照片资产。

01:46)修改和删除需要重新检查 App 已经记录的资产。

let fetchResult = PHAsset.fetchAssets(with: localIdentifiers, options: nil)
// Discover all modified and deleted assets
fetchResult.enumerateObjects { asset, idx, stop in
    if asset.modificationDate?.compare(lastLaunchDate) == .orderedDescending {
        // Asset could have been modified
    }
    if !localIdentifiers.contains(asset.localIdentifier) {
        // Asset could have been deleted
    }
}

关键点:

  • localIdentifiers 是 App 已经追踪的照片本地标识符。
  • fetchAssets(with:options:) 重新从照片库取出这些资产。
  • modificationDate 晚于上次启动时,只能说明资产可能被修改,因为系统内部处理也可能更新这个日期。
  • localIdentifier 不在本地记录里时,App 才能推断某个资产可能已经被删除。

这段代码体现了旧方案的成本:新增、修改、删除各走一套逻辑,结果仍然不够确定。

新做法:用 token 拉取持久变化

04:33)新的入口是 PHPhotoLibrary.fetchPersistentChanges(since:)。App 传入上次保存的 token,PhotoKit 返回 token 之后的 persistent changes。

let persistentChanges = try! PHPhotoLibrary.shared().fetchPersistentChanges(since: self.lastStoredToken)

for persistentChange in persistentChanges {
   if let changeDetails = persistentChange.changeDetails(for: PHObjectType.asset) {
        let updatedIdentifiers = changeDetails.updatedLocalIdentifiers
        let deletedIdentifiers = changeDetails.deletedLocalIdentifiers
        let insertedIdentifiers = changeDetails.insertedLocalIdentifiers
    }
}

// After processing change details
self.lastStoredToken = lastPersistentChange.changeToken

关键点:

  • lastStoredToken 是上次处理完照片库变化后保存的 persistent change token。
  • fetchPersistentChanges(since:) 返回这个 token 之后发生的变化。
  • changeDetails(for: PHObjectType.asset) 只取照片资产相关的变化详情。
  • updatedLocalIdentifiersdeletedLocalIdentifiersinsertedLocalIdentifiers 分别给出更新、删除、新增的资产标识符。
  • 处理完成后,把最后一个 persistent change 的 changeToken 保存起来,供下次启动使用。

04:21changeDetails(for:) 支持三类 Photos 对象:asset、asset collection、collection list。这个 session 的示例只处理 PHObjectType.asset,因为登山拼贴关心的是照片资产本身。

只处理跟 App 有关的变化

05:55)拿到变化列表后,App 仍然要过滤。登山 App 不需要照片库里的所有变化,只关心三件事:新增照片是否落在某次 hike 的时间段内,已用照片是否被编辑,已用照片是否被删除。

// Get last stored change token
let changeToken = self.lastStoredToken

// Fetch persistent changes
let persistentChanges = try!
         library.fetchPersistentChanges(since: changeToken)

for persistentChange in persistentChanges {
    // Grab change details and process updates
}

关键点:

  • changeToken 代表 App 上次同步到的照片库状态。
  • library.fetchPersistentChanges(since:) 一次取回后续变化。
  • 循环里再按 App 的业务规则处理变化详情,避免把照片库所有内部变化都变成 UI 工作。

新增照片:用 inserted identifiers 生成新拼贴

06:51)对于新增资产,PhotoKit 已经给出 insertedIdentifiers。App 可以取出这些资产,再用 hike 的时间范围判断它们是否属于某次运动。

let insertedAssets = PHAsset.fetchAssets(with: insertedIdentifiers, options: nil)
insertedAssets.enumerateObjects { asset, idx, stop in
   for hike in hikes {
        let dateInterval = NSDateInterval(start: hike.startDate, end: hike.endDate)
        if dateInterval.contains(asset.creationDate) {
            // This hike contains a new added asset
        }
    }
}

关键点:

  • insertedIdentifiers 来自 persistent change details。
  • PHAsset.fetchAssets(with:options:) 把标识符转成可读取的 PHAsset
  • 每个 hike 都有开始和结束时间。
  • NSDateInterval 用这两个时间构造一个区间。
  • dateInterval.contains(asset.creationDate) 判断新增照片是否属于这次 hike。

修改照片:用 hasAdjustments 判断是否需要重绘

07:08)对于更新资产,session 引入了新的 hasAdjustments API。它可以判断资产是否有编辑调整,登山 App 用它决定是否重绘 UI 里的照片。

let updatedAssets = PHAsset.fetchAssets(with: updatedIdentifiers, options: nil)
updatedAssets.enumerateObjects { asset, idx, stop in
    if asset.hasAdjustments {
        // This asset has edits
    }
}

关键点:

  • updatedIdentifiers 来自 persistent change details。
  • fetchAssets 取回这些被更新的资产。
  • asset.hasAdjustments 表示这张资产有编辑调整。
  • 条件成立时,App 可以重新渲染使用这张照片的界面。

删除照片:用 deleted identifiers 找到要重建的拼贴

07:20)删除资产时,PhotoKit 给出的本地标识符已经足够。App 不需要再从照片库 fetch 一个不存在的资产,只要检查哪些拼贴引用了这些标识符。

for deletedIdentifier in deletedIdentifiers {
    for collage in collages {
        if collage.assetLocalIdentifiers.contains(deletedIdentifier) {
            // This collage needs to be redrawn
        }
    }
}

关键点:

  • deletedIdentifiers 是已删除资产的本地标识符集合。
  • collages 是 App 本地保存的拼贴数据。
  • assetLocalIdentifiers 记录某个拼贴使用了哪些照片。
  • 找到匹配的 deletedIdentifier 后,这个拼贴需要重新生成。

错误处理:token 过期或详情不可用时重新 fetch

08:20)Persistent history 可能返回两类错误。第一类是 persistentChangeTokenExpired,表示 token 比可用历史还旧。第二类是 persistentChangeDetailsUnavailable,表示这次 persistent change 不能完整重建发生过的变化。

do {
    let persistentChanges = try library.fetchPersistentChanges(since: changeToken)
} catch PHPhotosError.persistentChangeTokenExpired,
        PHPhotosError.persistentChangeDetailsUnavailable {
    let fetchResult = PHAsset.fetchAssets(with: trackedIdentifiers, options: options)
    // Use fetch result
}

关键点:

  • do 块里正常按 token 拉取 persistent changes。
  • persistentChangeTokenExpired 需要 App 放弃旧 token。
  • persistentChangeDetailsUnavailable 需要 App 重新确认当前照片库状态。
  • 回退方案是用 trackedIdentifiers 重新 fetch 已追踪对象,让 App 恢复到正确状态。

Apple 还建议把 change history 查询放到后台线程,避免照片库大量同步和处理活动阻塞 UI。对于 inserted 和 updated assets,一次大 fetch 通常比多次小 fetch 更合适。(07:36

其他 PhotoKit 更新

08:54)Session 末尾还提到几项 PhotoKit 更新:可以通过 media subtype 和 smart album 访问 cinematic videos;新增两个错误码,分别覆盖 macOS 上照片图库位于 File Provider sync root directory 时的图库损坏场景,以及 asset resource 因网络问题无法找到的场景。


核心启发

  • 做什么:给旅行、运动或亲子相册 App 做自动补图。
    为什么值得做:persistent change history 可以直接告诉 App 哪些照片在上次启动后新增。
    怎么开始:保存 lastStoredToken,用 insertedLocalIdentifiers fetch 新资产,再按 creationDate 匹配活动时间段。

  • 做什么:让照片拼贴、封面图或时间线在用户编辑照片后自动更新。
    为什么值得做updatedLocalIdentifiershasAdjustments 可以定位被编辑的资产。
    怎么开始:维护每个 UI 模块引用的 asset local identifiers,检测更新后只重绘受影响模块。

  • 做什么:为相册整理工具做删除感知的本地索引。
    为什么值得做deletedLocalIdentifiers 直接给出已删除资产,不需要反复全量 diff。
    怎么开始:本地数据库保存 asset local identifier,收到删除标识符后清理索引或标记待重建。

  • 做什么:给长时间不打开的照片 App 做后台同步入口。
    为什么值得做:persistent history 记录 App 不活跃期间的长时间变化。
    怎么开始:在后台队列调用 fetchPersistentChanges(since:),只处理业务需要的 asset、asset collection 或 collection list 变化。

  • 做什么:为 limited library mode 下的照片功能做增量刷新。
    为什么值得做:PhotoKit 只返回用户已选择对象的变化,适合权限范围较小的 App。
    怎么开始:把同步逻辑建立在 token 和 local identifiers 上,不假设 App 能访问整个照片库。


关联 Session

评论

GitHub Issues · utterances