This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
121 lines
3.9 KiB
Swift
121 lines
3.9 KiB
Swift
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)
|
|
}
|
|
}
|