Add scan flow MVP and local Axiom skill workspace
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.
This commit is contained in:
120
StackDex/Services/PhotoLibraryService.swift
Normal file
120
StackDex/Services/PhotoLibraryService.swift
Normal file
@@ -0,0 +1,120 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user