Tusker/Tusker/Caching/ImageCache.swift

141 lines
4.9 KiB
Swift

//
// ImageCache.swift
// Tusker
//
// Created by Shadowfacts on 8/21/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
#if os(visionOS)
private let imageScale: CGFloat = 2
#else
@MainActor
private let imageScale = UIScreen.main.scale
#endif
final class ImageCache: @unchecked Sendable {
@MainActor
static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24 * 7), desiredSize: CGSize(width: 50, height: 50))
@MainActor
static let headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60 * 24 * 7))
@MainActor
static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
@MainActor
static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60 * 24 * 7))
#if DEBUG
private static let disableCaching = ProcessInfo.processInfo.environment.keys.contains("DISABLE_IMAGE_CACHE")
#else
private static let disableCaching = false
#endif
private let cache: ImageDataCache
private let desiredPixelSize: CGSize?
@MainActor
init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) {
// todo: might not always want to use UIScreen.main for this, e.g. Catalyst?
let pixelSize = desiredSize?.applying(.init(scaleX: imageScale, y: imageScale))
self.desiredPixelSize = pixelSize
self.cache = ImageDataCache(name: name, memoryExpiry: memoryExpiry, diskExpiry: diskExpiry, storeOriginalDataInMemory: diskExpiry == nil, desiredPixelSize: pixelSize)
}
func get(_ url: URL, loadOriginal: Bool = false, completion: (@Sendable (Data?, UIImage?) -> Void)?) -> Request? {
if !ImageCache.disableCaching,
let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) {
completion?(entry.data, entry.image)
return nil
} else {
return getFromSource(url, completion: completion)
}
}
func getFromSource(_ url: URL, completion: (@Sendable (Data?, UIImage?) -> Void)?) -> Request? {
return Task.detached(priority: .userInitiated) {
let result = await self.fetch(url: url)
switch result {
case .data(let data):
completion?(data, nil)
case .dataAndImage(let data, let image):
completion?(data, image)
case .none:
completion?(nil, nil)
}
}
}
func get(_ url: URL, loadOriginal: Bool = false) async -> (Data?, UIImage?) {
if !ImageCache.disableCaching,
let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) {
return (entry.data, entry.image)
} else {
let result = await self.fetch(url: url)
switch result {
case .data(let data):
return (data, nil)
case .dataAndImage(let data, let image):
return (data, image)
case .none:
return (nil, nil)
}
}
}
func fetchIfNotCached(_ url: URL) {
// if caching is disabled, don't bother fetching since nothing will be done with the result
guard !ImageCache.disableCaching else { return }
if !((try? cache.has(url.absoluteString)) ?? false) {
Task.detached(priority: .medium) {
_ = await self.fetch(url: url)
}
}
}
private func fetch(url: URL) async -> FetchResult {
guard let (data, _) = try? await URLSession.shared.data(from: url) else {
return .none
}
guard let image = UIImage(data: data) else {
try? cache.set(url.absoluteString, data: data, image: nil)
return .data(data)
}
let preparedImage: UIImage?
if let desiredPixelSize {
preparedImage = await image.byPreparingThumbnail(ofSize: desiredPixelSize)
} else {
preparedImage = await image.byPreparingForDisplay()
}
try? cache.set(url.absoluteString, data: data, image: preparedImage ?? image)
return .dataAndImage(data, preparedImage ?? image)
}
func getData(_ url: URL) -> Data? {
return try? cache.getData(url.absoluteString)
}
func get(_ url: URL, loadOriginal: Bool = false) -> ImageDataCache.Entry? {
return try? cache.get(url.absoluteString, loadOriginal: loadOriginal)
}
func reset() throws {
try cache.removeAll()
}
func getDiskSizeInBytes() -> Int64? {
return cache.disk?.getSizeInBytes()
}
typealias Request = Task<Void, Never>
enum FetchResult {
case data(Data)
case dataAndImage(Data, UIImage)
case none
}
}