damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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 }