Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
Derived
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public actor AsyncMultiplexImageNukeDownloader: AsyncMultiplexImageDownloader {
public func download(
candidate: AsyncMultiplexImageCandidate,
displaySize: CGSize
) async throws -> UIImage {
) async throws -> DownloadResult {

#if DEBUG

Expand All @@ -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
}

}
76 changes: 58 additions & 18 deletions Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -153,6 +187,7 @@ private struct _AsyncMultiplexImage<
}

@State private var item: ResultContainer.ItemSwiftUI?

@State private var displaySize: CGSize = .zero
@Environment(\.displayScale) var displayScale

Expand All @@ -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)
)
Expand All @@ -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
}
Expand Down Expand Up @@ -274,7 +314,7 @@ private struct _AsyncMultiplexImage<

await MainActor.run {
self.item = .init(
source: imageRepresentation,
representation: imageRepresentation,
phase: item.swiftUI
)
}
Expand All @@ -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)
)

}
Expand Down
4 changes: 2 additions & 2 deletions Sources/AsyncMultiplexImage/AsyncMultiplexImageContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions Sources/AsyncMultiplexImage/AsyncMultiplexImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
26 changes: 13 additions & 13 deletions Sources/AsyncMultiplexImage/DownloadManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

}
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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 {
Expand Down