Complete rename across all bundle IDs, App Groups, CloudKit containers, StoreKit product IDs, data store filenames, URL schemes, logger subsystems, Swift identifiers, user-facing strings (7 languages), file names, directory names, Xcode project, schemes, assets, and documentation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
182 lines
4.7 KiB
Swift
182 lines
4.7 KiB
Swift
//
|
|
// ImageCache.swift
|
|
// Reflect
|
|
//
|
|
// In-memory image cache for thumbnail images to improve scrolling performance.
|
|
//
|
|
|
|
import UIKit
|
|
import SwiftUI
|
|
|
|
/// Thread-safe in-memory image cache
|
|
final class ImageCache {
|
|
static let shared = ImageCache()
|
|
|
|
private let cache = NSCache<NSString, UIImage>()
|
|
private let queue = DispatchQueue(label: "com.88oakapps.reflect.imagecache", qos: .userInitiated)
|
|
|
|
private var memoryWarningToken: NSObjectProtocol?
|
|
|
|
private init() {
|
|
// Configure cache limits
|
|
cache.countLimit = 100 // Max 100 images
|
|
cache.totalCostLimit = 50 * 1024 * 1024 // 50 MB max
|
|
|
|
// Clear cache on memory warning
|
|
memoryWarningToken = NotificationCenter.default.addObserver(
|
|
forName: UIApplication.didReceiveMemoryWarningNotification,
|
|
object: nil,
|
|
queue: .main
|
|
) { [weak self] _ in
|
|
self?.clearCache()
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
if let token = memoryWarningToken {
|
|
NotificationCenter.default.removeObserver(token)
|
|
}
|
|
}
|
|
|
|
// MARK: - Public API
|
|
|
|
/// Get image from cache
|
|
func image(forKey key: String) -> UIImage? {
|
|
queue.sync {
|
|
cache.object(forKey: key as NSString)
|
|
}
|
|
}
|
|
|
|
/// Store image in cache
|
|
func setImage(_ image: UIImage, forKey key: String) {
|
|
queue.async { [weak self] in
|
|
let cost = Int(image.size.width * image.size.height * 4) // Approximate memory cost
|
|
self?.cache.setObject(image, forKey: key as NSString, cost: cost)
|
|
}
|
|
}
|
|
|
|
/// Remove image from cache
|
|
func removeImage(forKey key: String) {
|
|
queue.async { [weak self] in
|
|
self?.cache.removeObject(forKey: key as NSString)
|
|
}
|
|
}
|
|
|
|
/// Clear all cached images
|
|
func clearCache() {
|
|
queue.async { [weak self] in
|
|
self?.cache.removeAllObjects()
|
|
}
|
|
}
|
|
|
|
// MARK: - Convenience Methods for Photo IDs
|
|
|
|
/// Get cached image for photo ID
|
|
func image(forPhotoID id: UUID) -> UIImage? {
|
|
image(forKey: id.uuidString)
|
|
}
|
|
|
|
/// Store image for photo ID
|
|
func setImage(_ image: UIImage, forPhotoID id: UUID) {
|
|
setImage(image, forKey: id.uuidString)
|
|
}
|
|
|
|
/// Remove cached image for photo ID
|
|
func removeImage(forPhotoID id: UUID) {
|
|
removeImage(forKey: id.uuidString)
|
|
}
|
|
}
|
|
|
|
// MARK: - Cached Photo Loading
|
|
|
|
extension PhotoManager {
|
|
/// Load thumbnail with caching
|
|
func loadCachedThumbnail(id: UUID) -> UIImage? {
|
|
// Check cache first
|
|
if let cached = ImageCache.shared.image(forPhotoID: id) {
|
|
return cached
|
|
}
|
|
|
|
// Load from disk
|
|
guard let image = loadThumbnail(id: id) else {
|
|
return nil
|
|
}
|
|
|
|
// Cache for future use
|
|
ImageCache.shared.setImage(image, forPhotoID: id)
|
|
return image
|
|
}
|
|
|
|
/// Load full image with caching
|
|
func loadCachedPhoto(id: UUID) -> UIImage? {
|
|
let cacheKey = "\(id.uuidString)-full"
|
|
|
|
// Check cache first
|
|
if let cached = ImageCache.shared.image(forKey: cacheKey) {
|
|
return cached
|
|
}
|
|
|
|
// Load from disk
|
|
guard let image = loadPhoto(id: id) else {
|
|
return nil
|
|
}
|
|
|
|
// Cache for future use
|
|
ImageCache.shared.setImage(image, forKey: cacheKey)
|
|
return image
|
|
}
|
|
}
|
|
|
|
// MARK: - SwiftUI Cached Image View
|
|
|
|
struct CachedAsyncImage: View {
|
|
let photoID: UUID?
|
|
let useThumbnail: Bool
|
|
|
|
@State private var image: UIImage?
|
|
@State private var isLoading = false
|
|
|
|
init(photoID: UUID?, useThumbnail: Bool = true) {
|
|
self.photoID = photoID
|
|
self.useThumbnail = useThumbnail
|
|
}
|
|
|
|
var body: some View {
|
|
Group {
|
|
if let image = image {
|
|
Image(uiImage: image)
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fill)
|
|
} else if isLoading {
|
|
ProgressView()
|
|
} else {
|
|
Color.gray.opacity(0.2)
|
|
}
|
|
}
|
|
.onAppear {
|
|
loadImage()
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private func loadImage() {
|
|
guard let id = photoID else { return }
|
|
isLoading = true
|
|
|
|
Task {
|
|
let loadedImage = await Task.detached(priority: .userInitiated) {
|
|
if useThumbnail {
|
|
return await PhotoManager.shared.loadCachedThumbnail(id: id)
|
|
} else {
|
|
return await PhotoManager.shared.loadCachedPhoto(id: id)
|
|
}
|
|
}.value
|
|
|
|
await MainActor.run {
|
|
self.image = loadedImage
|
|
self.isLoading = false
|
|
}
|
|
}
|
|
}
|
|
}
|