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:
Matthias
2026-04-19 21:11:32 +02:00
parent 577214d474
commit a60a76b797
679 changed files with 138964 additions and 73 deletions

View 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)
}
}