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==" | |
)! | |
} |
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
I pass them in via the initializer for the sake of dependency injection. I've actually got a protocol that my image manager expects and my actual image cache, which conforms comes from another module.
With that said, I don't have to do it this way. I was trying to keep things separate, but I'm not sure it's worth the trouble in this case.
Ok, well in the process of working through this with you, and some additional exploration on other adjacent topics, I've started to question whether some of my core approaches are even providing me benefits for all the complexities they add. I think now is a good time for me to consider throwing the baby out with the bathwater on some things. I want to spend some time on that, and then come back to this to see if these concerns are even relevant in the new world.
However, the cache itself, the use of
sending
, and the exploration of reentrancy has been invaluable and will certainly live on in my future code in some form. I really appreciate your help on this.