Skip to content

Instantly share code, notes, and snippets.

@cameronmcefee
Last active October 31, 2024 11:28
Show Gist options
  • Save cameronmcefee/fc78db3e62276dd112780cae3f566e20 to your computer and use it in GitHub Desktop.
Save cameronmcefee/fc78db3e62276dd112780cae3f566e20 to your computer and use it in GitHub Desktop.
import SwiftUI
#if canImport(UIKit)
import UIKit
public typealias PlatformImage = UIImage
#elseif canImport(AppKit)
import AppKit
public typealias PlatformImage = NSImage
#endif
// -------------- //
// Relevant Class //
// -------------- //
// The cache is just a simple cache, a little sugar on top of NSCache.
actor PlatformImageCache {
private var cache: NSCache<NSString, PlatformImage> = .init()
init() {}
func key(_ id: UUID) -> NSString {
NSString(string: id.uuidString)
}
public func get(id: UUID) -> PlatformImage? {
cache.object(forKey: key(id))
}
public func set(_ image: PlatformImage, as id: UUID) {
cache.setObject(image, forKey: key(id))
}
}
// The ImageManager will handle actually acquiring the data needed. The
// .image(for: UUID) method is the external api that the image views will call to get their needed images.
@MainActor
@Observable
final class ImageManager {
let cache: PlatformImageCache
private let database: DatabaseActor
private var activeTasks: [UUID: Task<Void, Never>] = [:]
nonisolated init(cache: PlatformImageCache, database: DatabaseActor) {
self.cache = cache
self.database = database
}
func image(for id: UUID) async -> PlatformImage? {
if let image = await cache.get(id: id) {
return image
}
if let activeTask = activeTasks[id] {
_ = await activeTask.value
return await cache.get(id: id)
}
let task = Task {
guard let data = await database.getData(for: id) else { return }
guard let image = await decodeDataIntoImage(data: data) else { return }
await cache.set(image, as: id)
activeTasks.removeValue(forKey: id)
}
activeTasks[id] = task
_ = await task.value
return await cache.get(id: id)
}
private nonisolated func decodeDataIntoImage(
data: Data
) async -> sending PlatformImage? {
try? await Task.sleep(for: .seconds(1))
return PlatformImage(data: data)
}
}
// This would be a @ModelActor I can fetch the actual data from
actor DatabaseActor {
func getData(for _: UUID) -> Data? {
Data.example
}
}
// -------------- //
// Usage Context //
// -------------- //
struct ContentView: View {
@State private var demoUUID = UUID()
var body: some View {
VStack {
ForEach(0 ..< 10, id: \.self) { index in
ImageView(id: demoUUID, index: index)
}
}
}
}
struct ImageView: View {
@Environment(\.appController) private var app
@State private var platformImage: PlatformImage?
let id: UUID
let index: Int
var body: some View {
ZStack {
Text(String(index))
if let platformImage {
Image(platformImage: platformImage)
} else {
ProgressView()
}
}
.task {
platformImage = await app.imageManager.image(for: id)
}
}
}
@MainActor
@Observable
final class AppController {
let imageManager: ImageManager
nonisolated init() {
let database = DatabaseActor()
let cache = PlatformImageCache()
imageManager = ImageManager(cache: cache, database: database)
}
}
extension EnvironmentValues {
@MainActor
@Entry var appController: AppController = .init()
}
extension SwiftUI.Image {
init(platformImage: PlatformImage) {
#if os(macOS)
self.init(nsImage: platformImage)
#else
self.init(uiImage: platformImage)
#endif
}
}
#Preview {
@Previewable @State var appController = AppController()
ContentView()
.environment(\.appController, appController)
}
extension Data {
static let example =
Data(
base64Encoded: "iVBORw0KGgoAAAANSUhEUgAAAD4AAAA5CAYAAABuxJj8AAAAAXNSR0IArs4c6QAAAIRlWElmTU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAACQAAAAAQAAAJAAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAAD6gAwAEAAAAAQAAADkAAAAAhTsXfgAAAAlwSFlzAAAWJQAAFiUBSVIk8AAAABxpRE9UAAAAAgAAAAAAAAAdAAAAKAAAAB0AAAAcAAABnsjR3d0AAAFqSURBVGgFYmAYBaMhMBoCNA4BVhqbP+iM1wO66BYQ/wHiJUDMAsTDHjACfXgNiP8j4bJh72ugB92RPAzz/GOgGChAhjWYC/QdzMPItPVw9jUz0HOvcXi8dzh73BGHp0Exf284e3wyHo+DPG84HD0PKrxAhRhyvkZnNw9Hj5sT8DQoEK4OR493EuFxkOfVh5vnbxPp8crh5HFdIj0NivHTw8nj9SR4HOR5ueHi+Yskejx/OHhcmURPg2L84HDweCkZHv8L1CM21D1/nAyPg2I9dSA8zgS0tACITwAxqLX1ggL8D6gX5BFS8VcK7AS59w4QbwRiCyAmGhQCVZLq0MGqHhSAksT6fNUw8jgoQtyI9XjSMPL4G6BfBIn1OEhdPxAP1uRLrLveAf1gB/IMqSAKqOEjEBNr0WBSB6pNlEj1MLJ6RSDnGBAPJk/hcwuoDdACxFQZsgYZAhokABmKz9KBlnsEdJ89EBMFAAAAAP//699psgAAAexJREFU7ZhPKwVRGMavP6GUWNgpK1dKKZIsFMJSysbCjg9g4RMoISQlJBs2FizvJ7BgxcLKRkRdIlmgRP783sXU3GmMc+bec+ao+9bTnZn7nud9nnfOzJmZVEo9eki9Bt8O4gBNdcBY1MK8D1wx/4qWSWNuQ4gnOPYCkmzAKfWbQ7QZP5Smwgmwbf6LmkugAiQWUnwRiBgbDbilzhBwJgZRkgUmzWfgr3fGsU/ImmHj7b5azmyWokSmockzPueMW5+QXsOmpaEXvnrObK5bMC7mO5xxjJAycAdMTnOPe8El4/2WTIv5S5eMb1o0LuY7XTAv0/zesnF5YEo8BlDgXX86v2+MO4459opxiccWCnQMS+45aAMlYBq8A12OLsYkFuVUfgA6onfIrw4olmtW1mgdnuUAh9VdeVlQFftM7niEuhr+29Pgkw8iMmMSiW2qqhiX9+YmRYXyni8fF1R4uxU5C5om0/xRQeAqOZWalVvIP1PgXtHkLUj6X9NcmjKcR6Uqxm6AqDN/kwd/7KHzEaIO+a8hNnPuwFF2n8BvDUjnppvfC1vGPik7A+ShppDRCNkRCDNvfVkbCQiRu2wfMBVyT5kFH8BrQJZtuSSsxxgVd8EUCK7NpsS0Qiw3NXkNVl0pTGkp8hY78B868AMXvm1f7P57MAAAAABJRU5ErkJggg=="
)!
}
@mattmassicotte
Copy link

Well, I'm very happy to help!

I think what you are saying here is very wise. Decomposing components into little parts that are assembled together is a super powerful pattern. But, tht flexibiility does definitely, sometimes, come with trade-offs. I think this particular trade-off is silly, because you're fighting against an isolation mismatch that I don't think makes sense. But, we cannot change the framework.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment