Last active
October 31, 2024 11:28
-
-
Save cameronmcefee/fc78db3e62276dd112780cae3f566e20 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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==" | |
)! | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.