import UIKit /// Accounts' pictures, decoded and kept while the app runs, so that a screen /// draws those it has seen in its first frame. Each is asked for once, however /// many rows show it. The address names the picture's content, so the session's /// disk cache holds them across launches too. actor Faces { static let shared = Faces() private static let limit = 64 * 1024 * 1024 // NSCache is safe across threads, though it isn't marked Sendable. private nonisolated(unsafe) static let store: NSCache = { let cache = NSCache() cache.totalCostLimit = limit return cache }() private var loading: [URL: Task] = [:] private nonisolated(unsafe) static let addresses = NSCache() /// Where an account's picture is, where an answer has said. nonisolated static func address(of did: String) -> String? { addresses.object(forKey: did as NSString) as String? } nonisolated static func keep(address: String, of did: String) { addresses.setObject(address as NSString, forKey: did as NSString) } nonisolated static func forget() { store.removeAllObjects() } nonisolated static func kept(_ url: URL) -> UIImage? { store.object(forKey: url as NSURL) } func load(_ url: URL) async throws -> UIImage { if let face = Self.kept(url) { return face } // Not tied to the row asking: another may be waiting on the same one. let task = loading[url] ?? Task.detached { let data = try await Client.shared.picture(url) guard let image = UIImage(data: data) else { throw URLError(.cannotDecodeContentData) } let face = image.preparingForDisplay() ?? image let bytes = face.cgImage.map { $0.bytesPerRow * $0.height } ?? data.count Self.store.setObject(face, forKey: url as NSURL, cost: bytes) return face } loading[url] = task defer { loading[url] = nil } return try await task.value } }