ImageCache.swift (5193B)
1 // 2 // ImageCache.swift 3 // damus 4 // 5 // Created by William Casarin on 2022-05-04. 6 // 7 8 import Foundation 9 import SwiftUI 10 import UIKit 11 12 enum ImageProcessingStatus { 13 case processing 14 case done 15 } 16 17 class ImageCache { 18 private let lock = NSLock() 19 private var state: [String: ImageProcessingStatus] = [:] 20 21 private func get_state(_ key: String) -> ImageProcessingStatus? { 22 lock.lock(); defer { lock.unlock() } 23 24 return state[key] 25 } 26 27 private func set_state(_ key: String, new_state: ImageProcessingStatus) { 28 lock.lock(); defer { lock.unlock() } 29 30 state[key] = new_state 31 } 32 33 lazy var cache: NSCache<NSString, UIImage> = { 34 let cache = NSCache<NSString, UIImage>() 35 cache.totalCostLimit = 1024 * 1024 * 100 // 100MB 36 return cache 37 }() 38 39 // simple polling until I can figure out a better way to do this 40 func wait_for_image(_ key: String) async { 41 while true { 42 let why_would_this_happen: ()? = try? await Task.sleep(nanoseconds: 100_000_000) // 100ms 43 if why_would_this_happen == nil { 44 return 45 } 46 if get_state(key) == .done { 47 return 48 } 49 } 50 } 51 52 func lookup_sync(key: String) -> UIImage? { 53 let status = get_state(key) 54 55 switch status { 56 case .done: 57 break 58 case .processing: 59 return nil 60 case .none: 61 return nil 62 } 63 64 if let decoded = cache.object(forKey: NSString(string: key)) { 65 return decoded 66 } 67 68 return nil 69 } 70 71 func lookup_or_load_image(key: String, url: URL?) async -> UIImage? { 72 if let img = await lookup(key: key) { 73 return img 74 } 75 76 guard let url = url else { 77 return nil 78 } 79 80 return await load_image(cache: self, from: url, key: key) 81 } 82 83 func get_cache_url(key: String, suffix: String, ext: String = "png") -> URL? { 84 let urls = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask) 85 86 guard let root = urls.first else { 87 return nil 88 } 89 90 return root.appendingPathComponent("\(key)\(suffix).\(ext)") 91 } 92 93 private func lookup_file_cache(key: String, suffix: String = "_pfp") -> UIImage? { 94 guard let img_file = get_cache_url(key: key, suffix: suffix) else { 95 return nil 96 } 97 98 guard let img = UIImage(contentsOfFile: img_file.path) else { 99 //print("failed to load \(key)\(suffix).png from file cache") 100 return nil 101 } 102 103 save_to_memory_cache(key: key, img: img) 104 105 return img 106 } 107 108 func lookup(key: String) async -> UIImage? { 109 let status = get_state(key) 110 111 switch status { 112 case .done: 113 break 114 case .processing: 115 await wait_for_image(key) 116 case .none: 117 return lookup_file_cache(key: key) 118 } 119 120 if let decoded = cache.object(forKey: NSString(string: key)) { 121 return decoded 122 } 123 124 return nil 125 } 126 127 func remove(key: String) { 128 lock.lock(); defer { lock.unlock() } 129 cache.removeObject(forKey: NSString(string: key)) 130 } 131 132 func insert(_ image: UIImage, key: String) async -> UIImage? { 133 let scale = await UIScreen.main.scale 134 let size = CGSize(width: PFP_SIZE * scale, height: PFP_SIZE * scale) 135 136 set_state(key, new_state: .processing) 137 138 let decoded_image = await image.byPreparingThumbnail(ofSize: size) 139 140 save_to_memory_cache(key: key, img: decoded_image ?? UIImage()) 141 if let img = decoded_image { 142 if !save_to_file_cache(key: key, img: img) { 143 print("failed saving \(key) pfp to file cache") 144 } 145 } 146 147 return decoded_image 148 } 149 150 func save_to_file_cache(key: String, img: UIImage, suffix: String = "_pfp") -> Bool { 151 guard let url = get_cache_url(key: key, suffix: suffix) else { 152 return false 153 } 154 155 guard let data = img.pngData() else { 156 return false 157 } 158 159 return (try? data.write(to: url)) != nil 160 } 161 162 func save_to_memory_cache(key: String, img: UIImage) { 163 lock.lock() 164 cache.setObject(img, forKey: NSString(string: key)) 165 state[key] = .done 166 lock.unlock() 167 } 168 } 169 170 func load_image(cache: ImageCache, from url: URL, key: String) async -> UIImage? { 171 guard let (data, _) = try? await URLSession.shared.data(from: url) else { 172 return nil 173 } 174 175 guard let img = UIImage(data: data) else { 176 return nil 177 } 178 179 return await cache.insert(img, key: key) 180 } 181 182 183 func hashed_hexstring(_ str: String) -> String { 184 guard let data = str.data(using: .utf8) else { 185 return str 186 } 187 188 return hex_encode(sha256(data)) 189 } 190 191 func pfp_cache_key(url: URL) -> String { 192 return hashed_hexstring(url.absoluteString) 193 }