黑苹果macOS PhotosKit相册应用开发完全指南
发布时间:2026年6月12日 | 分类:黑苹果 | 关键词:PhotosKit, PHPicker, PHAsset, 相册管理, 图像处理
前言:macOS照片生态与PhotosKit框架
在macOS上,照片管理一直是一个重要的用户场景。无论是从iPhone同步的照片、数码相机导入的RAW文件,还是截图和设计素材,用户需要一个高效的方式来管理和处理这些图像资源。Apple的Photos.app虽然功能强大,但它的封闭性使得第三方开发者很难与之集成。
PhotosKit框架就是为了解决这个问题而生的。它提供了对照片库的完整读写权限,允许第三方应用访问、修改和管理用户的照片资源——当然,这一切都需要经过用户明确授权。对于黑苹果开发者来说,PhotosKit的应用场景非常丰富:从专业的照片编辑器到智能相册整理工具,从批量图片处理到AI驱动的图像识别应用。
本文将系统性地介绍PhotosKit的核心API,包括PHPicker(照片选择器)、PHAsset(照片资源)、PHCachingImageManager(高性能缩略图)、PHPhotoLibrary(照片库变更监听)以及自定义编辑扩展的开发。
第一章:PHPickerViewController——现代照片选择器
1.1 基本用法
PHPicker是Apple推荐的现代照片选择方式,它不需要完全的照片库访问权限,更加隐私友好:
import PhotosUI
import SwiftUI
struct PhotoPickerView: View {
@State private var selectedImages: [UIImage] = []
@State private var isPresented = false
var body: some View {
VStack {
ScrollView(.horizontal) {
HStack {
ForEach(selectedImages.indices, id: \.self) { index in
Image(uiImage: selectedImages[index])
.resizable()
.scaledToFit()
.frame(height: 150)
}
}
}
Button("选择照片") {
isPresented = true
}
}
.sheet(isPresented: $isPresented) {
PhotoPicker(images: $selectedImages)
}
}
}
// UIKit包装
struct PhotoPicker: UIViewControllerRepresentable {
@Binding var images: [UIImage]
func makeUIViewController(context: Context) -> PHPickerViewController {
var config = PHPickerConfiguration()
config.selectionLimit = 0 // 0表示无限制
config.filter = .images // 只选择图片
let picker = PHPickerViewController(configuration: config)
picker.delegate = context.coordinator
return picker
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, PHPickerViewControllerDelegate {
let parent: PhotoPicker
init(_ parent: PhotoPicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true)
for result in results {
let provider = result.itemProvider
if provider.canLoadObject(ofClass: UIImage.self) {
provider.loadObject(ofClass: UIImage.self) { image, error in
if let image = image as? UIImage {
DispatchQueue.main.async {
self.parent.images.append(image)
}
}
}
}
}
}
}
}第二章:PHAsset——照片资源的完整生命周期管理
2.1 获取照片资源
PHAsset代表了照片库中的一个资源(图片或视频)。通过PHFetchResult可以批量查询:
import Photos
// 请求授权
func requestAuthorization() async -> Bool {
let status = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
return status == .authorized
}
// 获取所有照片
func fetchAllPhotos() -> PHFetchResult<PHAsset> {
let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
options.predicate = NSPredicate(format: "mediaType == %d", PHAssetMediaType.image.rawValue)
return PHAsset.fetchAssets(with: options)
}
// 按相册获取
func fetchAlbum(named title: String) -> PHFetchResult<PHAsset>? {
let albums = PHAssetCollection.fetchAssetCollections(
with: .album, subtype: .any, options: nil
)
albums.enumerateObjects { collection, _, stop in
if collection.localizedTitle == title {
stop.pointee = true
}
}
return nil // 简化示例
}
// 按日期范围获取
func fetchPhotosInDateRange(from start: Date, to end: Date) -> PHFetchResult<PHAsset> {
let options = PHFetchOptions()
options.predicate = NSPredicate(
format: "creationDate >= %@ AND creationDate <= %@",
start as NSDate, end as NSDate
)
return PHAsset.fetchAssets(with: .image, options: options)
}2.2 PHAsset的元数据访问
PHAsset提供了丰富的元数据信息:
extension PHAsset {
var detailedInfo: String {
let formatter = ByteCountFormatter()
var info = ""
info += "本地标识符: \(self.localIdentifier)\n"
info += "像素尺寸: \(self.pixelWidth) x \(self.pixelHeight)\n"
info += "创建日期: \(self.creationDate ?? Date())\n"
info += "修改日期: \(self.modificationDate ?? Date())\n"
info += "时长: \(self.duration)秒\n"
info += "媒体类型: \(self.mediaType == .image ? "图片" : "视频")\n"
info += "来源类型: \(self.sourceType.rawValue)\n"
info += "是否收藏: \(self.isFavorite ? "是" : "否")\n"
info += "是否隐藏: \(self.isHidden ? "是" : "否")\n"
info += "连拍标识符: \(self.burstIdentifier ?? "无")\n"
return info
}
}第三章:PHCachingImageManager——高性能图片加载
3.1 缩略图与全尺寸图加载
直接加载全分辨率图片会导致内存溢出。PHCachingImageManager提供了按需加载不同尺寸图片的能力:
class ThumbnailCache {
private let imageManager = PHCachingImageManager()
func loadThumbnail(for asset: PHAsset, size: CGSize, completion: @escaping (UIImage?) -> Void) {
let options = PHImageRequestOptions()
options.deliveryMode = .opportunistic // 先返回低质量,再返回高质量
options.isNetworkAccessAllowed = true // 允许从iCloud下载
options.isSynchronous = false
options.resizeMode = .fast
imageManager.requestImage(
for: asset,
targetSize: size,
contentMode: .aspectFill,
options: options
) { image, info in
let isDegraded = (info?[PHImageResultIsDegradedKey] as? Bool) ?? false
// 只使用高质量版本
if !isDegraded {
completion(image)
}
}
}
func preloadAssets(_ assets: [PHAsset], targetSize: CGSize) {
imageManager.startCachingImages(
for: assets,
targetSize: targetSize,
contentMode: .aspectFill,
options: nil
)
}
func stopCaching() {
imageManager.stopCachingImagesForAllAssets()
}
}3.2 加载原始数据与EXIF信息
专业照片应用通常需要读取EXIF、IPTC等元数据:
func loadImageData(for asset: PHAsset) async -> (Data?, [String: Any]?) {
return await withCheckedContinuation { continuation in
let options = PHImageRequestOptions()
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) {
data, uti, orientation, info in
continuation.resume(returning: (data, info))
}
}
}
// 读取EXIF信息
func extractEXIF(from asset: PHAsset) async -> [String: Any]? {
let options = PHContentEditingInputRequestOptions()
options.isNetworkAccessAllowed = true
return await withCheckedContinuation { continuation in
asset.requestContentEditingInput(with: options) { input, _ in
guard let url = input?.fullSizeImageURL else {
continuation.resume(returning: nil)
return
}
if let source = CGImageSourceCreateWithURL(url as CFURL, nil),
let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any] {
continuation.resume(returning: properties)
} else {
continuation.resume(returning: nil)
}
}
}
}第四章:PHPhotoLibrary——照片库变更监听与写入
4.1 监听照片库变更
当用户在Photos.app中修改照片时,你的应用可以实时响应:
class PhotoLibraryObserver: NSObject, PHPhotoLibraryChangeObserver {
func startObserving() {
PHPhotoLibrary.shared().register(self)
}
func photoLibraryDidChange(_ changeInstance: PHChange) {
DispatchQueue.main.async {
// 响应变更
if let changes = changeInstance.changeDetails(for: self.currentFetchResult) {
self.currentFetchResult = changes.fetchResultAfterChanges
if changes.hasIncrementalChanges {
// 增量更新
if let removed = changes.removedIndexes {
print("删除了 \(removed.count) 张照片")
}
if let inserted = changes.insertedIndexes {
print("新增了 \(inserted.count) 张照片")
}
if let changed = changes.changedIndexes {
print("修改了 \(changed.count) 张照片")
}
} else {
// 全量刷新
print("照片库发生重大变更,需要重新加载")
}
}
}
}
private var currentFetchResult: PHFetchResult<PHAsset>!
}4.2 写入照片到相册
将处理后的图片保存到照片库:
func saveImageToLibrary(_ image: UIImage, albumName: String? = nil) async throws -> String {
var localIdentifier: String = ""
try await PHPhotoLibrary.shared().performChanges {
let request = PHAssetChangeRequest.creationRequestForAsset(from: image)
localIdentifier = request.placeholderForCreatedAsset?.localIdentifier ?? ""
// 如果指定了相册名,添加到指定相册
if let albumName = albumName {
let albums = PHAssetCollection.fetchAssetCollections(
with: .album, subtype: .any, options: nil
)
var targetAlbum: PHAssetCollection?
albums.enumerateObjects { collection, _, stop in
if collection.localizedTitle == albumName {
targetAlbum = collection
stop.pointee = true
}
}
if let album = targetAlbum,
let addRequest = PHAssetCollectionChangeRequest(for: album) {
addRequest.addAssets([request.placeholderForCreatedAsset!] as NSFastEnumeration)
}
}
}
return localIdentifier
}第五章:构建高级智能相册——结合Vision框架
5.1 图像内容识别与自动分类
将PhotosKit与Vision框架结合,可以实现AI驱动的智能相册:
import Vision
func classifyPhotos(_ assets: [PHAsset]) async {
for asset in assets {
guard let image = await loadImage(for: asset) else { continue }
let request = VNClassifyImageRequest()
let handler = VNImageRequestHandler(cgImage: image.cgImage!, options: [:])
do {
try handler.perform([request])
if let observations = request.results {
let topCategories = observations
.sorted { $0.confidence > $1.confidence }
.prefix(5)
.map { "\($0.identifier): \(Int($0.confidence * 100))%" }
print("照片分类: \(topCategories.joined(separator: ", "))")
}
} catch {
print("分类失败: \(error)")
}
}
}5.2 人脸分组与人物相册
利用Vision的人脸检测将照片按人物自动分组——这在黑苹果上同样可以高效运行,因为Vision框架完全利用Metal GPU加速:
func detectFaces(in asset: PHAsset) async -> Int {
guard let image = await loadImage(for: asset) else { return 0 }
let request = VNDetectFaceRectanglesRequest()
let handler = VNImageRequestHandler(cgImage: image.cgImage!, options: [:])
do {
try handler.perform([request])
return request.results?.count ?? 0
} catch {
return 0
}
}第六章:自定义照片编辑扩展
6.1 创建Photo Editing Extension
macOS支持创建照片编辑扩展,让用户在Photos.app中直接使用你的编辑工具:
class PhotoEditingViewController: NSViewController, PHContentEditingController {
var input: PHContentEditingInput?
// 开始编辑
func startContentEditing(with contentEditingInput: PHContentEditingInput,
placeholderImage: NSImage) {
input = contentEditingInput
// 加载完整分辨率图片
if let url = contentEditingInput.fullSizeImageURL {
let image = NSImage(contentsOf: url)
// 显示图片供用户编辑...
}
}
// 完成编辑
func finishContentEditing(completionHandler: @escaping (PHContentEditingOutput?) -> Void) {
guard let input = input else {
completionHandler(nil)
return
}
let output = PHContentEditingOutput(contentEditingInput: input)
// 将编辑后的JPEG写入output.renderedContentURL
// 保存调整数据(用于非破坏性编辑)
let adjustmentData = PHAdjustmentData(
formatIdentifier: "com.yourapp.photoedit",
formatVersion: "1.0",
data: Data() // 你的调整参数
)
output.adjustmentData = adjustmentData
completionHandler(output)
}
}总结
PhotosKit为macOS应用提供了完整的照片管理能力。从隐私友好的PHPicker到高性能的PHCachingImageManager,从照片库实时监听到自定义编辑扩展,这个框架涵盖了照片应用开发的方方面面。
对于黑苹果开发者来说,PhotosKit的一个重要价值在于——它让macOS上的第三方照片工具可以与iOS设备的照片库无缝同步(通过iCloud照片库)。这意味着你开发的Mac应用看到的照片,和用户iPhone上拍摄的照片是同一份数据,大大提升了用户体验。
结合Vision框架进行AI驱动的照片分析、利用Metal进行图像处理加速、通过CloudKit实现跨设备元数据同步——这些高级功能的组合,可以创造出远超Photos.app的专业照片管理和编辑工具。希望本文能帮助你在黑苹果环境中充分利用PhotosKit构建出色的照片应用!


评论(0)