import Combine import Photos import PhotosUI import SwiftUI import UIKit struct RecentPhotoItem: Identifiable, Equatable { let id: String let asset: PHAsset let thumbnail: UIImage } @MainActor final class PhotoLibraryService: NSObject, ObservableObject { @Published private(set) var authorizationStatus: PHAuthorizationStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite) @Published private(set) var recentPhotos: [RecentPhotoItem] = [] @Published private(set) var isLoading = false private let imageManager = PHCachingImageManager() var canBrowseRecents: Bool { authorizationStatus == .authorized || authorizationStatus == .limited } var statusMessage: String { switch authorizationStatus { case .authorized: return "Recent photos" case .limited: return "Limited photo access" case .denied: return "Photo access denied" case .restricted: return "Photo access restricted" case .notDetermined: return "Show recent photos" @unknown default: return "Photo access unavailable" } } func requestAccessAndLoad() async { if authorizationStatus == .notDetermined { authorizationStatus = await PHPhotoLibrary.requestAuthorization(for: .readWrite) } await refreshRecentsIfPossible() } func refreshRecentsIfPossible() async { authorizationStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite) guard canBrowseRecents else { recentPhotos = [] return } isLoading = true defer { isLoading = false } let options = PHFetchOptions() options.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] options.fetchLimit = 12 let assets = PHAsset.fetchAssets(with: .image, options: options) var results: [RecentPhotoItem] = [] assets.enumerateObjects { asset, _, _ in results.append(contentsOf: self.thumbnailItem(for: asset).map { [$0] } ?? []) } recentPhotos = results } func loadImage(for item: RecentPhotoItem) async -> UIImage? { await withCheckedContinuation { continuation in let options = PHImageRequestOptions() options.deliveryMode = .highQualityFormat options.resizeMode = .fast options.isNetworkAccessAllowed = true imageManager.requestImageDataAndOrientation(for: item.asset, options: options) { data, _, _, _ in continuation.resume(returning: data.flatMap(UIImage.init(data:))) } } } func presentLimitedLibraryPicker() { guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let rootViewController = scene.keyWindow?.rootViewController else { return } PHPhotoLibrary.shared().presentLimitedLibraryPicker(from: rootViewController) } func openSettings() { guard let url = URL(string: UIApplication.openSettingsURLString) else { return } UIApplication.shared.open(url) } private func thumbnailItem(for asset: PHAsset) -> RecentPhotoItem? { let targetSize = CGSize(width: 180, height: 180) let options = PHImageRequestOptions() options.deliveryMode = .opportunistic options.resizeMode = .fast options.isSynchronous = true var thumbnailImage: UIImage? imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { image, _ in thumbnailImage = image } guard let thumbnailImage else { return nil } return RecentPhotoItem(id: asset.localIdentifier, asset: asset, thumbnail: thumbnailImage) } } private extension UIWindowScene { var keyWindow: UIWindow? { windows.first(where: \.isKeyWindow) } }