Files
Reflect/Shared/Services/ImageCache.swift
Trey t e5656f47fd Rename iFeels to Feels across entire codebase
- Bundle IDs: com.tt.ifeel* → com.tt.feels*
- App Groups: group.com.tt.ifeel* → group.com.tt.feels*
- iCloud containers: iCloud.com.tt.ifeel* → iCloud.com.tt.feels*
- IAP product IDs: com.tt.ifeel.IAP.* → com.tt.feels.IAP.*
- URLs: ifeels.app → feels.app
- Logger subsystems and dispatch queues
- Product names and display names

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 11:57:44 -06:00

174 lines
4.4 KiB
Swift

//
// ImageCache.swift
// Feels
//
// 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.tt.feels.imagecache", qos: .userInitiated)
private init() {
// Configure cache limits
cache.countLimit = 100 // Max 100 images
cache.totalCostLimit = 50 * 1024 * 1024 // 50 MB max
// Clear cache on memory warning
NotificationCenter.default.addObserver(
forName: UIApplication.didReceiveMemoryWarningNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.clearCache()
}
}
// 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
}
}
}
}