// // PhotoPickerStore.swift // QuickLocation // import UIKit import Photos struct PhotoAlbum { let collection: PHAssetCollection let title: String let count: Int let coverAsset: PHAsset? } final class PhotoPickerStore { private let imageManager = PHCachingImageManager() private let imageOptions: PHImageRequestOptions = { let options = PHImageRequestOptions() options.deliveryMode = .opportunistic options.resizeMode = .fast options.isNetworkAccessAllowed = true return options }() private(set) var albums: [PhotoAlbum] = [] private(set) var assets: [PHAsset] = [] func requestAccess(_ completion: @escaping (Bool) -> Void) { let finish: (PHAuthorizationStatus) -> Void = { status in DispatchQueue.main.async { if #available(iOS 14, *) { completion(status == .authorized || status == .limited) } else { completion(status == .authorized) } } } if #available(iOS 14, *) { let status = PHPhotoLibrary.authorizationStatus(for: .readWrite) if status == .notDetermined { PHPhotoLibrary.requestAuthorization(for: .readWrite, handler: finish) } else { finish(status) } } else { let status = PHPhotoLibrary.authorizationStatus() if status == .notDetermined { PHPhotoLibrary.requestAuthorization(finish) } else { finish(status) } } } func reloadAlbums() { var list: [PhotoAlbum] = [] let smart = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .any, options: nil) smart.enumerateObjects { collection, _, _ in guard let album = self.makeAlbum(collection) else { return } if collection.assetCollectionSubtype == .smartAlbumUserLibrary { list.insert(album, at: 0) } else { list.append(album) } } let user = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: nil) user.enumerateObjects { collection, _, _ in guard let album = self.makeAlbum(collection) else { return } list.append(album) } albums = list if let first = albums.first { loadAssets(in: first) } else { assets = [] } } func loadAssets(in album: PhotoAlbum) { assets = fetchImages(in: album.collection) } func requestThumbnail(for asset: PHAsset, size: CGSize, completion: @escaping (UIImage?) -> Void) -> PHImageRequestID { let scale = UIScreen.main.scale let target = CGSize(width: size.width * scale, height: size.height * scale) return imageManager.requestImage(for: asset, targetSize: target, contentMode: .aspectFill, options: imageOptions) { image, info in let degraded = (info?[PHImageResultIsDegradedKey] as? Bool) ?? false if image != nil || !degraded { completion(image) } } } func cancelRequest(_ id: PHImageRequestID) { imageManager.cancelImageRequest(id) } func requestFullImages(for assets: [PHAsset], completion: @escaping ([UIImage]) -> Void) { guard !assets.isEmpty else { completion([]) return } let options = PHImageRequestOptions() options.deliveryMode = .highQualityFormat options.resizeMode = .none options.isNetworkAccessAllowed = true options.isSynchronous = false let group = DispatchGroup() var images: [UIImage?] = Array(repeating: nil, count: assets.count) var finished = Array(repeating: false, count: assets.count) let lock = NSLock() let maxSide = max(UIScreen.main.bounds.width, UIScreen.main.bounds.height) * UIScreen.main.scale * 2 let target = CGSize(width: maxSide, height: maxSide) for (index, asset) in assets.enumerated() { group.enter() imageManager.requestImage(for: asset, targetSize: target, contentMode: .aspectFit, options: options) { image, info in let degraded = (info?[PHImageResultIsDegradedKey] as? Bool) ?? false let cancelled = (info?[PHImageCancelledKey] as? Bool) ?? false let error = info?[PHImageErrorKey] as? Error lock.lock() let already = finished[index] if !already, !degraded || image == nil || cancelled || error != nil { finished[index] = true if !degraded { images[index] = image } lock.unlock() group.leave() } else { lock.unlock() } } } group.notify(queue: .main) { completion(images.compactMap { $0 }) } } private func makeAlbum(_ collection: PHAssetCollection) -> PhotoAlbum? { if collection.assetCollectionSubtype == .smartAlbumAllHidden { return nil } let hiddenTitles: Set = ["最近删除", "Recently Deleted", "Hidden", "已隐藏"] if let title = collection.localizedTitle, hiddenTitles.contains(title) { return nil } let images = fetchImages(in: collection) guard !images.isEmpty else { return nil } let title = collection.localizedTitle?.isEmpty == false ? collection.localizedTitle! : "相册" return PhotoAlbum(collection: collection, title: title, count: images.count, coverAsset: images.first) } private func fetchImages(in collection: PHAssetCollection) -> [PHAsset] { let options = PHFetchOptions() options.predicate = NSPredicate(format: "mediaType == %d", PHAssetMediaType.image.rawValue) options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] let result = PHAsset.fetchAssets(in: collection, options: options) var list: [PHAsset] = [] list.reserveCapacity(result.count) result.enumerateObjects { asset, _, _ in list.append(asset) } return list } }