diff --git a/Development/AsyncMultiplexImage-Demo.xcodeproj/xcshareddata/xcschemes/AsyncMultiplexImage-Demo.xcscheme b/Development/AsyncMultiplexImage-Demo.xcodeproj/xcshareddata/xcschemes/AsyncMultiplexImage-Demo.xcscheme index f6c7b6a..e2de900 100644 --- a/Development/AsyncMultiplexImage-Demo.xcodeproj/xcshareddata/xcschemes/AsyncMultiplexImage-Demo.xcscheme +++ b/Development/AsyncMultiplexImage-Demo.xcodeproj/xcshareddata/xcschemes/AsyncMultiplexImage-Demo.xcscheme @@ -53,7 +53,7 @@ + isEnabled = "NO"> diff --git a/Development/AsyncMultiplexImage-Demo/ContentView.swift b/Development/AsyncMultiplexImage-Demo/ContentView.swift index 2a4d83e..dac5854 100644 --- a/Development/AsyncMultiplexImage-Demo/ContentView.swift +++ b/Development/AsyncMultiplexImage-Demo/ContentView.swift @@ -45,60 +45,21 @@ actor _SlowDownloader: AsyncMultiplexImageDownloader { struct ContentView: View { - @State private var basePhotoURLString: String = - "https://images.unsplash.com/photo-1492446845049-9c50cc313f00" - var body: some View { NavigationView { Form { Section { NavigationLink("SwiftUI") { - VStack { - AsyncMultiplexImage( - multiplexImage: .init( - identifier: basePhotoURLString, - urls: buildURLs(basePhotoURLString) - ), - downloader: _SlowDownloader(pipeline: .shared) - ) { phase in - switch phase { - case .empty: - Text("Loading") - case .progress(let image): - image - .resizable() - .scaledToFill() - case .success(let image): - image - .resizable() - .scaledToFill() - case .failure(let error): - Text("Error") - } - } - - HStack { - Button("1") { - basePhotoURLString = - "https://images.unsplash.com/photo-1660668377331-da480e5339a0" - } - Button("2") { - basePhotoURLString = - "https://images.unsplash.com/photo-1658214764191-b002b517e9e5" - } - Button("3") { - basePhotoURLString = - "https://images.unsplash.com/photo-1587126396803-be14d33e49cf" - } - } - } - .padding() - .navigationTitle("SwiftUI") + SwitchingDemo() + .navigationTitle("SwiftUI") } NavigationLink("UIKit") { UIKitContentViewRepresentable() } - NavigationLink("SwiftUI List", destination: { UsingList() }) + + NavigationLink("Stress 1", destination: { StressGrid() }) + + NavigationLink("Stress 2", destination: { StressGrid() }) } .navigationTitle("Multiplex Image") } @@ -106,6 +67,42 @@ struct ContentView: View { } } +private struct SwitchingDemo: View { + + @State private var basePhotoURLString: String = + "https://images.unsplash.com/photo-1492446845049-9c50cc313f00" + + var body: some View { + VStack { + AsyncMultiplexImage( + multiplexImage: .init( + identifier: basePhotoURLString, + urls: buildURLs(basePhotoURLString) + ), + downloader: _SlowDownloader(pipeline: .shared), + content: AsyncMultiplexImageBasicContent() + ) + + HStack { + Button("1") { + basePhotoURLString = + "https://images.unsplash.com/photo-1660668377331-da480e5339a0" + } + Button("2") { + basePhotoURLString = + "https://images.unsplash.com/photo-1658214764191-b002b517e9e5" + } + Button("3") { + basePhotoURLString = + "https://images.unsplash.com/photo-1587126396803-be14d33e49cf" + } + } + } + .padding() + } + +} + struct UIKitContentViewRepresentable: UIViewRepresentable { func makeUIView(context: Context) -> UIKitContentView { diff --git a/Development/AsyncMultiplexImage-Demo/List.swift b/Development/AsyncMultiplexImage-Demo/List.swift index 0159ad7..b4d199a 100644 --- a/Development/AsyncMultiplexImage-Demo/List.swift +++ b/Development/AsyncMultiplexImage-Demo/List.swift @@ -1,54 +1,66 @@ import AsyncMultiplexImage import AsyncMultiplexImage_Nuke -// -// List.swift -// AsyncMultiplexImage-Demo -// -// Created by Muukii on 2024/06/13. -// import SwiftUI -struct UsingList: View { +struct StressGrid: View { @State var items: [Entity] = Entity.batch() var body: some View { - ScrollView { - LazyVGrid( - columns: [ - .init(.flexible(minimum: 0, maximum: .infinity), spacing: 16), - .init(.flexible(minimum: 0, maximum: .infinity), spacing: 16), - .init(.flexible(minimum: 0, maximum: .infinity), spacing: 16), - .init(.flexible(minimum: 0, maximum: .infinity), spacing: 16), - ], spacing: 16 - ) { - ForEach(items) { entity in - if entity.id == items.last?.id { - Cell(entity: entity) - .onAppear { - Task { - let newItems = await Entity.delayBatch() - items.append(contentsOf: newItems) - } - } - } else { - Cell(entity: entity) + GeometryReader { proxy in + ScrollView { + LazyVGrid( + columns: [ + .init(.flexible(minimum: 0, maximum: .infinity), spacing: 2), + .init(.flexible(minimum: 0, maximum: .infinity), spacing: 2), + .init(.flexible(minimum: 0, maximum: .infinity), spacing: 2), + .init(.flexible(minimum: 0, maximum: .infinity), spacing: 2), + ], spacing: 2 + ) { + ForEach(items) { entity in + Cell(entity: entity) } } } + .onPreferenceChange(AnchorPreferenceKey.self, perform: { v in + guard let v = v else { + return + } + let bounds = proxy[v] + print(bounds) + }) } } } #Preview { - UsingList() + StressGrid() +} + +#Preview { + StressGrid() } let imageURLString = "https://images.unsplash.com/photo-1567095761054-7a02e69e5c43?q=80&w=800&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D" -struct Cell: View { +protocol CellType: View { + init(entity: Entity) +} + +struct Cell_1: View, CellType { + + let entity: Entity + + var body: some View { + AsyncMultiplexImageNuke(imageRepresentation: .remote(entity.image)) + .frame(height: 100) + } +} + + +struct Cell_2: View, CellType { let entity: Entity @@ -56,12 +68,49 @@ struct Cell: View { VStack { AsyncMultiplexImageNuke(imageRepresentation: .remote(entity.image)) .frame(height: 100) - HStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text(entity.name) - } + .clipShape( + RoundedRectangle( + cornerRadius: 20, + style: .continuous + ) + ) + .frame(maxWidth: .infinity) + .aspectRatio(1, contentMode: .fit) + } + .padding() + } +} + +struct Cell_3: View, CellType { + + final class Object: ObservableObject { + + @Published var value: Int = 0 + + init() { + print("Object.init") + } + + deinit { +// print("Object.deinit") + + } + } + + let entity: Entity + + @State private var value: Int = 0 + @StateObject private var object = Object() + + var body: some View { + let _ = Self._printChanges() + VStack { + Button("Up \(value)") { + value += 1 + } + Button("Up \(object.value)") { + object.value += 1 + } } .padding() } @@ -84,7 +133,7 @@ struct Entity: Identifiable { } static func batch() -> [Self] { - (0..<100).map { _ in + (0..<100000).map { _ in .make() } } diff --git a/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNuke.swift b/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNuke.swift index 17f918a..323a857 100644 --- a/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNuke.swift +++ b/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNuke.swift @@ -12,27 +12,9 @@ public struct AsyncMultiplexImageNuke: View { public var body: some View { AsyncMultiplexImage( imageRepresentation: imageRepresentation, - downloader: AsyncMultiplexImageNukeDownloader.shared - ) { phase in - Group { - switch phase { - case .empty: - EmptyView() - case .progress(let image): - image - .resizable() - .scaledToFill() - .transition(.opacity.animation(.bouncy)) - case .success(let image): - image - .resizable() - .scaledToFill() - .transition(.opacity.animation(.bouncy)) - case .failure: - EmptyView() - } - } - } + downloader: AsyncMultiplexImageNukeDownloader.shared, + content: AsyncMultiplexImageBasicContent() + ) } } @@ -52,11 +34,11 @@ public struct AsyncMultiplexImageNuke: View { #Preview("Rotating") { HStack { - + Rectangle() .frame(width: 100, height: 100) .rotationEffect(.degrees(10)) - + AsyncMultiplexImageNuke( imageRepresentation: .remote( .init( @@ -70,7 +52,7 @@ public struct AsyncMultiplexImageNuke: View { .frame(width: 100, height: 100) .rotationEffect(.degrees(10)) .clipped(antialiased: true) - + AsyncMultiplexImageNuke( imageRepresentation: .remote( .init( @@ -83,7 +65,7 @@ public struct AsyncMultiplexImageNuke: View { ) .frame(width: 100, height: 100) .rotationEffect(.degrees(20)) - + AsyncMultiplexImageNuke( imageRepresentation: .remote( .init( diff --git a/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNukeDownloader.swift b/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNukeDownloader.swift index 39b0e90..d6b2957 100644 --- a/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNukeDownloader.swift +++ b/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNukeDownloader.swift @@ -4,7 +4,7 @@ import Nuke import SwiftUI public actor AsyncMultiplexImageNukeDownloader: AsyncMultiplexImageDownloader { - + public static let `shared` = AsyncMultiplexImageNukeDownloader(pipeline: .shared, debugDelay: 0) public let pipeline: ImagePipeline diff --git a/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift b/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift index 459bb01..9084349 100644 --- a/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift +++ b/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift @@ -44,17 +44,17 @@ extension OSLog { @inline(__always) private static func makeOSLogInDebug(isEnabled: Bool = true, _ factory: () -> OSLog) -> OSLog { #if DEBUG - if ProcessInfo.processInfo.environment["ASYNC_MULTIPLEX_IMAGE_LOG_ENABLED"] == "1" { - return factory() - } else { - return .disabled - } + if ProcessInfo.processInfo.environment["ASYNC_MULTIPLEX_IMAGE_LOG_ENABLED"] == "1" { + return factory() + } else { + return .disabled + } #else - return .disabled + return .disabled #endif } - static let generic: OSLog = makeOSLogInDebug { + static let generic: OSLog = makeOSLogInDebug(isEnabled: false) { OSLog.init(subsystem: "app.muukii", category: "default") } static let view: OSLog = makeOSLogInDebug { @@ -67,13 +67,6 @@ extension OSLog { } -@MainActor -public final class DownloadManager { - - public static let shared: DownloadManager = .init() - -} - public protocol AsyncMultiplexImageDownloader: Actor { func download(candidate: AsyncMultiplexImageCandidate, displaySize: CGSize) async throws @@ -100,21 +93,22 @@ public enum ImageRepresentation: Equatable { case loaded(Image) } -public struct AsyncMultiplexImage: View { +public struct AsyncMultiplexImage< + Content: AsyncMultiplexImageContent, Downloader: AsyncMultiplexImageDownloader +>: View { private let imageRepresentation: ImageRepresentation private let downloader: Downloader - private let content: (AsyncMultiplexImagePhase) -> Content + private let content: Content + private let clearsContentBeforeDownload: Bool - // sharing - @StateObject private var viewModel: _AsyncMultiplexImageViewModel = .init() // convenience init public init( multiplexImage: MultiplexImage, downloader: Downloader, clearsContentBeforeDownload: Bool = true, - @ViewBuilder content: @escaping (AsyncMultiplexImagePhase) -> Content + content: Content ) { self.init( imageRepresentation: .remote(multiplexImage), @@ -128,7 +122,7 @@ public struct AsyncMultiplexImage Content + content: Content ) { self.clearsContentBeforeDownload = clearsContentBeforeDownload @@ -140,7 +134,6 @@ public struct AsyncMultiplexImage? - - func registerCurrentTask(_ task: Task?) { - self.cancelCurrentTask() - self.task = task +public enum AnchorPreferenceKey: PreferenceKey { + public static var defaultValue: Anchor? { + nil } - - func cancelCurrentTask() { - guard let task else { return } - guard task.isCancelled == false else { return } - task.cancel() + public static func reduce(value: inout Value, nextValue: () -> Value) { + value = nextValue() } - - deinit { - guard let task else { return } - guard task.isCancelled == false else { return } - task.cancel() - } - } -private struct _AsyncMultiplexImage: View -{ +private struct _AsyncMultiplexImage< + Content: AsyncMultiplexImageContent, Downloader: AsyncMultiplexImageDownloader +>: View { private struct UpdateTrigger: Equatable { let size: CGSize let image: ImageRepresentation } - @State private var currentUsingCandidates: [AsyncMultiplexImageCandidate] = [] - @State private var item: ResultContainer.ItemSwiftUI? - - let viewModel: _AsyncMultiplexImageViewModel + @State private var displaySize: CGSize = .zero + @Environment(\.displayScale) var displayScale private let imageRepresentation: ImageRepresentation private let downloader: Downloader - private let content: (AsyncMultiplexImagePhase) -> Content + private let content: Content private let clearsContentBeforeDownload: Bool public init( - viewModel: _AsyncMultiplexImageViewModel, clearsContentBeforeDownload: Bool, imageRepresentation: ImageRepresentation, downloader: Downloader, - @ViewBuilder content: @escaping (AsyncMultiplexImagePhase) -> Content + content: Content ) { - self.viewModel = viewModel self.clearsContentBeforeDownload = clearsContentBeforeDownload self.imageRepresentation = imageRepresentation self.downloader = downloader @@ -210,34 +185,42 @@ private struct _AsyncMultiplexImage 0 && newSize.width > 0 else { return } - + if clearsContentBeforeDownload { var transaction = Transaction() transaction.disablesAnimations = true @@ -245,190 +228,78 @@ private struct _AsyncMultiplexImage? - private var progressImagesTask: Task? - - deinit { - idealImageTask?.cancel() - progressImagesTask?.cancel() - } - - func makeStream( - candidates: [AsyncMultiplexImageCandidate], - downloader: Downloader, - displaySize: CGSize - ) -> AsyncThrowingStream { - - Log.debug(.`generic`, "Load: \(candidates.map { $0.urlRequest })") - - return .init { [self] continuation in - - continuation.onTermination = { [self] termination in - - switch termination { - case .finished, .cancelled: - Task { - await self.idealImageTask?.cancel() - await self.progressImagesTask?.cancel() - } - @unknown default: - break - } - - } - - guard let idealCandidate = candidates.first else { - continuation.finish() - return - } - - let idealImage = Task { - - do { - let result = try await downloader.download( - candidate: idealCandidate, - displaySize: displaySize - ) - - progressImagesTask?.cancel() - - Log.debug(.`generic`, "Loaded ideal") - - lastCandidate = idealCandidate - continuation.yield(.final(result)) - } catch { - continuation.yield(with: .failure(error)) - } - - continuation.finish() - - } - - idealImageTask = idealImage - - let progressCandidates = candidates.dropFirst(1) - - guard progressCandidates.isEmpty == false else { - return - } - - let progressImages = Task { - - // download images sequentially from lower image - for candidate in progressCandidates.reversed() { - do { - + guard Task.isCancelled == false else { - Log.debug(.`generic`, "Cancelled progress images") return } - - Log.debug(.`generic`, "Load progress image => \(candidate.index)") - let result = try await downloader.download( - candidate: candidate, - displaySize: displaySize + + let stream = await DownloadManager.shared.start( + source: multiplexImage, + candidates: candidates, + downloader: downloader, + displaySize: newSize ) - + guard Task.isCancelled == false else { - Log.debug(.`generic`, "Cancelled progress images") - return - } - - if let lastCandidate, lastCandidate.index > candidate.index { - continuation.finish() return } - - lastCandidate = idealCandidate - - let yieldResult = continuation.yield(.progress(result)) - - Log.debug(.`generic`, "Loaded progress image => \(candidate.index), \(yieldResult)") - } catch { - + + do { + for try await item in stream { + + guard Task.isCancelled == false else { + return + } + + await MainActor.run { + self.item = item.swiftUI + } + } + } catch { + // FIXME: Error handling + } + + case .loaded(let image): + + self.item = .final(image) + } - } + } onCancel: { + print("cancel") + } + + }) + .clipped(antialiased: true) +// .onDisappear { +// self.task?.cancel() +// self.task = nil +// } - } + } - progressImagesTask = progressImages +} - } - } +private func pushBackground(task: @Sendable () -> sending Result) async -> sending Result { + task() } diff --git a/Sources/AsyncMultiplexImage/AsyncMultiplexImageContent.swift b/Sources/AsyncMultiplexImage/AsyncMultiplexImageContent.swift new file mode 100644 index 0000000..96a439c --- /dev/null +++ b/Sources/AsyncMultiplexImage/AsyncMultiplexImageContent.swift @@ -0,0 +1,34 @@ +import SwiftUI + +public protocol AsyncMultiplexImageContent { + + associatedtype Content: View + + @ViewBuilder + func body(phase: AsyncMultiplexImagePhase) -> Content +} + +public struct AsyncMultiplexImageBasicContent: AsyncMultiplexImageContent { + + public init() {} + + public func body(phase: AsyncMultiplexImagePhase) -> some View { + switch phase { + case .empty: + Rectangle().fill(.clear) + case .progress(let image): + image + .resizable() + .scaledToFill() + .transition(.opacity.animation(.bouncy)) + case .success(let image): + image + .resizable() + .scaledToFill() + .transition(.opacity.animation(.bouncy)) + case .failure: + Rectangle().fill(.clear) + } + } + +} diff --git a/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift b/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift index 2abf8ed..93c6990 100644 --- a/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift +++ b/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift @@ -33,7 +33,7 @@ open class AsyncMultiplexImageView: UIView { public let downloader: any AsyncMultiplexImageDownloader public let offloadStrategy: (any OffloadStrategy)? - private let viewModel: _AsyncMultiplexImageViewModel = .init() + private var task: Task? private var currentUsingNetworkImage: MultiplexImage? private var currentUsingImage: UIImage? @@ -109,7 +109,8 @@ open class AsyncMultiplexImageView: UIView { let offloads = offloadStrategy?.offloads(using: state) if let offloads, offloads { - viewModel.cancelCurrentTask() + self.task?.cancel() + self.task = nil unloadNetworkImage() } @@ -158,14 +159,19 @@ open class AsyncMultiplexImageView: UIView { currentUsingNetworkImage = nil currentUsingImage = image - viewModel.cancelCurrentTask() imageView.image = image + + self.task?.cancel() + self.task = nil + } public func clearImage() { currentUsingNetworkImage = nil imageView.image = nil - viewModel.cancelCurrentTask() + + self.task?.cancel() + self.task = nil } private func startDownload() { @@ -181,7 +187,12 @@ open class AsyncMultiplexImageView: UIView { } // making new candidates - let urls = image._urlsProvider(newSize) + let context = MultiplexImage.Context( + targetSize: newSize, + displayScale: UIScreen.main.scale + ) + + let urls = image.makeURLs(context: context) let candidates = urls.enumerated().map { i, e in AsyncMultiplexImageCandidate(index: i, urlRequest: .init(url: e)) @@ -234,7 +245,7 @@ open class AsyncMultiplexImageView: UIView { } } - viewModel.registerCurrentTask(currentTask) + self.task = currentTask } private func unloadNetworkImage() { diff --git a/Sources/AsyncMultiplexImage/DownloadManager.swift b/Sources/AsyncMultiplexImage/DownloadManager.swift new file mode 100644 index 0000000..254dccf --- /dev/null +++ b/Sources/AsyncMultiplexImage/DownloadManager.swift @@ -0,0 +1,181 @@ +// +// DownloadManager.swift +// AsyncMultiplexImage +// +// Created by Muukii on 2025/01/21. +// +import SwiftUI + +actor DownloadManager { + + @MainActor + static let shared = DownloadManager() + + private init() {} + + func start( + source: MultiplexImage, + candidates: [AsyncMultiplexImageCandidate], + downloader: any AsyncMultiplexImageDownloader, + displaySize: CGSize + ) async -> sending AsyncThrowingStream { + + // this instance will be alive until finish + let container = ResultContainer() + + let stream = await container.makeStream( + candidates: candidates, + downloader: downloader, + displaySize: displaySize + ) + + return stream + + } + +} + +actor ResultContainer { + + enum Item { + case progress(UIImage) + case final(UIImage) + + var swiftUI: ItemSwiftUI { + switch self { + case .progress(let image): + return .progress(.init(uiImage: image).renderingMode(.original)) + case .final(let image): + return .final(.init(uiImage: image).renderingMode(.original)) + } + } + } + + enum ItemSwiftUI { + case progress(Image) + case final(Image) + } + + private var referenceCount: UInt64 = 0 + + private var lastCandidate: AsyncMultiplexImageCandidate? = nil + + private var idealImageTask: Task? + private var progressImagesTask: Task? + + deinit { + idealImageTask?.cancel() + progressImagesTask?.cancel() + } + + func incrementReference() { + referenceCount += 1 + } + + func decrementReference() { + referenceCount -= 1 + } + + func makeStream( + candidates: [AsyncMultiplexImageCandidate], + downloader: Downloader, + displaySize: CGSize + ) -> AsyncThrowingStream { + + Log.debug(.`generic`, "Load: \(candidates.map { $0.urlRequest })") + + return .init { [self] continuation in + + continuation.onTermination = { [self] termination in + + switch termination { + case .finished, .cancelled: + Task { + await self.idealImageTask?.cancel() + await self.progressImagesTask?.cancel() + } + @unknown default: + break + } + + } + + guard let idealCandidate = candidates.first else { + continuation.finish() + return + } + + let idealImage = Task { + + do { + let result = try await downloader.download( + candidate: idealCandidate, + displaySize: displaySize + ) + + progressImagesTask?.cancel() + + Log.debug(.`generic`, "Loaded ideal") + + lastCandidate = idealCandidate + continuation.yield(.final(result)) + } catch { + continuation.yield(with: .failure(error)) + } + + continuation.finish() + + } + + idealImageTask = idealImage + + let progressCandidates = candidates.dropFirst(1) + + guard progressCandidates.isEmpty == false else { + return + } + + let progressImages = Task { + + // download images sequentially from lower image + for candidate in progressCandidates.reversed() { + do { + + guard Task.isCancelled == false else { + Log.debug(.`generic`, "Cancelled progress images") + return + } + + Log.debug(.`generic`, "Load progress image => \(candidate.index)") + let result = try await downloader.download( + candidate: candidate, + displaySize: displaySize + ) + + guard Task.isCancelled == false else { + Log.debug(.`generic`, "Cancelled progress images") + return + } + + if let lastCandidate, lastCandidate.index > candidate.index { + continuation.finish() + return + } + + lastCandidate = idealCandidate + + let yieldResult = continuation.yield(.progress(result)) + + Log.debug(.`generic`, "Loaded progress image => \(candidate.index), \(yieldResult)") + } catch { + + } + } + + } + + progressImagesTask = progressImages + + } + } +} diff --git a/Sources/AsyncMultiplexImage/MultiplexImage.swift b/Sources/AsyncMultiplexImage/MultiplexImage.swift index 2750290..84d589f 100644 --- a/Sources/AsyncMultiplexImage/MultiplexImage.swift +++ b/Sources/AsyncMultiplexImage/MultiplexImage.swift @@ -1,6 +1,19 @@ import Foundation -public struct MultiplexImage: Hashable { +public struct MultiplexImage: Hashable, Sendable { + + public struct Context: ~Copyable { + public let targetSize: CGSize + public let displayScale: CGFloat + + init( + targetSize: consuming CGSize, + displayScale: consuming CGFloat + ) { + self.targetSize = targetSize + self.displayScale = displayScale + } + } public static func == (lhs: MultiplexImage, rhs: MultiplexImage) -> Bool { lhs.identifier == rhs.identifier @@ -12,11 +25,16 @@ public struct MultiplexImage: Hashable { public let identifier: String - private(set) var _urlsProvider: @MainActor (CGSize) -> [URL] + private let _urlsProvider: @Sendable (borrowing Context) -> [URL] + /** + - Parameters: + - identifier: The unique identifier of the image. + - urlsProvider: The provider of the image URLs as the first item is the top priority. + */ public init( identifier: String, - urlsProvider: @escaping @MainActor (CGSize) -> [URL] + urlsProvider: @escaping @Sendable (borrowing Context) -> [URL] ) { self.identifier = identifier self._urlsProvider = urlsProvider @@ -25,7 +43,11 @@ public struct MultiplexImage: Hashable { public init(identifier: String, urls: [URL]) { self.init(identifier: identifier, urlsProvider: { _ in urls }) } - + + func makeURLs(context: borrowing Context) -> [URL] { + _urlsProvider(context) + } + } // MARK: convenience init