diff --git a/.gitignore b/.gitignore index 3b29812..a114d1e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ DerivedData/ .swiftpm/config/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc +Derived diff --git a/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNukeDownloader.swift b/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNukeDownloader.swift index 3180766..bb923f0 100644 --- a/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNukeDownloader.swift +++ b/Sources/AsyncMultiplexImage-Nuke/AsyncMultiplexImageNukeDownloader.swift @@ -21,7 +21,7 @@ public actor AsyncMultiplexImageNukeDownloader: AsyncMultiplexImageDownloader { public func download( candidate: AsyncMultiplexImageCandidate, displaySize: CGSize - ) async throws -> UIImage { + ) async throws -> DownloadResult { #if DEBUG @@ -43,9 +43,29 @@ public actor AsyncMultiplexImageNukeDownloader: AsyncMultiplexImageDownloader { ) ) - let result = try await task.image + let begin = CACurrentMediaTime() + + let result = try await task.response + + let end = CACurrentMediaTime() + + let took = end - begin + + var isFromCache: Bool { + switch result.cacheType { + case .memory, .disk: + return true + default: + return false + } + } + + return .init( + image: result.image, + isFromCache: false, + time: took + ) - return result } } diff --git a/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift b/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift index 41e8942..fa99abc 100644 --- a/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift +++ b/Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift @@ -67,17 +67,51 @@ extension OSLog { } +public struct DownloadResult: Sendable { + + public struct Metrics: Sendable, Equatable { + + public let isFromCache: Bool + public let time: TimeInterval + + } + + public let image: UIImage + public let metrics: Metrics + + public init( + image: UIImage, + isFromCache: Bool, + time: TimeInterval + ) { + self.image = image + self.metrics = .init( + isFromCache: isFromCache, + time: time + ) + + } +} + public protocol AsyncMultiplexImageDownloader: Actor { - func download(candidate: AsyncMultiplexImageCandidate, displaySize: CGSize) async throws - -> UIImage + func download( + candidate: AsyncMultiplexImageCandidate, + displaySize: CGSize + ) async throws + -> DownloadResult } +public enum Source: Equatable, Sendable { + case local + case remote(DownloadResult.Metrics) +} + public enum AsyncMultiplexImagePhase { case empty - case progress(Image) - case success(Image) + case progress(Image, Source) + case success(Image, Source) case failure(Error) } @@ -153,6 +187,7 @@ private struct _AsyncMultiplexImage< } @State private var item: ResultContainer.ItemSwiftUI? + @State private var displaySize: CGSize = .zero @Environment(\.displayScale) var displayScale @@ -174,21 +209,26 @@ private struct _AsyncMultiplexImage< self.content = content } + private static func phase(from: ResultContainer.ItemSwiftUI?) -> AsyncMultiplexImagePhase { + + guard let from else { + return .empty + } + + switch from.phase { + case .progress(let image, let source): + return .progress(image, source) + case .final(let image, let source): + return .success(image, source) + } + } + public var body: some View { Color.clear .overlay( content.body( - phase: { - switch item?.phase { - case .none: - return .empty - case .some(.progress(let image)): - return .progress(image) - case .some(.final(let image)): - return .success(image) - } - }() + phase: Self.phase(from: item) ) .frame(width: displaySize.width, height: displaySize.height) ) @@ -208,7 +248,7 @@ private struct _AsyncMultiplexImage< if let item, case .final = item.phase, - item.source == imageRepresentation { + item.representation == imageRepresentation { // already final item loaded return } @@ -274,7 +314,7 @@ private struct _AsyncMultiplexImage< await MainActor.run { self.item = .init( - source: imageRepresentation, + representation: imageRepresentation, phase: item.swiftUI ) } @@ -286,8 +326,8 @@ private struct _AsyncMultiplexImage< case .loaded(let image): self.item = .init( - source: imageRepresentation, - phase: .final(image) + representation: imageRepresentation, + phase: .final(image, .local) ) } diff --git a/Sources/AsyncMultiplexImage/AsyncMultiplexImageContent.swift b/Sources/AsyncMultiplexImage/AsyncMultiplexImageContent.swift index 96a439c..c40e5e6 100644 --- a/Sources/AsyncMultiplexImage/AsyncMultiplexImageContent.swift +++ b/Sources/AsyncMultiplexImage/AsyncMultiplexImageContent.swift @@ -16,12 +16,12 @@ public struct AsyncMultiplexImageBasicContent: AsyncMultiplexImageContent { switch phase { case .empty: Rectangle().fill(.clear) - case .progress(let image): + case .progress(let image, _): image .resizable() .scaledToFill() .transition(.opacity.animation(.bouncy)) - case .success(let image): + case .success(let image, _): image .resizable() .scaledToFill() diff --git a/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift b/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift index 93c6990..9e073fb 100644 --- a/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift +++ b/Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift @@ -226,9 +226,9 @@ open class AsyncMultiplexImageView: UIView { let transition = CATransition() transition.duration = 0.13 switch item { - case .progress(let image): + case .progress(let image, _): imageView.image = image - case .final(let image): + case .final(let image, _): imageView.image = image } self.layer.add(transition, forKey: "transition") diff --git a/Sources/AsyncMultiplexImage/DownloadManager.swift b/Sources/AsyncMultiplexImage/DownloadManager.swift index f7ca73a..fcd7143 100644 --- a/Sources/AsyncMultiplexImage/DownloadManager.swift +++ b/Sources/AsyncMultiplexImage/DownloadManager.swift @@ -37,28 +37,28 @@ actor DownloadManager { actor ResultContainer { - enum Item { - case progress(UIImage) - case final(UIImage) + enum Item: Sendable { + case progress(UIImage, Source) + case final(UIImage, Source) var swiftUI: ItemSwiftUI.Phase { switch self { - case .progress(let image): - return .progress(.init(uiImage: image).renderingMode(.original)) - case .final(let image): - return .final(.init(uiImage: image).renderingMode(.original)) + case .progress(let image, let source): + return .progress(.init(uiImage: image).renderingMode(.original), source) + case .final(let image, let source): + return .final(.init(uiImage: image).renderingMode(.original), source) } } } struct ItemSwiftUI: Equatable { - + enum Phase: Equatable { - case progress(Image) - case final(Image) + case progress(Image, Source) + case final(Image, Source) } - let source: ImageRepresentation + let representation: ImageRepresentation let phase: Phase } @@ -125,7 +125,7 @@ actor ResultContainer { Log.debug(.`generic`, "Loaded ideal") lastCandidate = idealCandidate - continuation.yield(.final(result)) + continuation.yield(.final(result.image, .remote(result.metrics))) } catch { continuation.yield(with: .failure(error)) } @@ -171,7 +171,7 @@ actor ResultContainer { lastCandidate = idealCandidate - let yieldResult = continuation.yield(.progress(result)) + let yieldResult = continuation.yield(.progress(result.image, .remote(result.metrics))) Log.debug(.`generic`, "Loaded progress image => \(candidate.index), \(yieldResult)") } catch {