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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
4B094D622D65B82500E4C4A9 /* ShrinkDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B094D612D65B82000E4C4A9 /* ShrinkDemo.swift */; };
4B094D642D65B82700E4C4A9 /* OptionalImageDemo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B094D632D65B82600E4C4A9 /* OptionalImageDemo.swift */; };
4B57105B28D0AC7F00AA053C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B57105A28D0AC7F00AA053C /* ContentView.swift */; };
4B57105D28D0AC8000AA053C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B57105C28D0AC8000AA053C /* Assets.xcassets */; };
4B57106128D0AC8000AA053C /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 4B57106028D0AC8000AA053C /* Preview Assets.xcassets */; };
Expand All @@ -21,6 +22,7 @@

/* Begin PBXFileReference section */
4B094D612D65B82000E4C4A9 /* ShrinkDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShrinkDemo.swift; sourceTree = "<group>"; };
4B094D632D65B82600E4C4A9 /* OptionalImageDemo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalImageDemo.swift; sourceTree = "<group>"; };
4B57105528D0AC7F00AA053C /* AsyncMultiplexImage-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "AsyncMultiplexImage-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; };
4B57105A28D0AC7F00AA053C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
4B57105C28D0AC8000AA053C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
Expand Down Expand Up @@ -68,6 +70,7 @@
isa = PBXGroup;
children = (
4B094D612D65B82000E4C4A9 /* ShrinkDemo.swift */,
4B094D632D65B82600E4C4A9 /* OptionalImageDemo.swift */,
4B57105A28D0AC7F00AA053C /* ContentView.swift */,
4B57105C28D0AC8000AA053C /* Assets.xcassets */,
4B57105E28D0AC8000AA053C /* AsyncMultiplexImage_Demo.entitlements */,
Expand Down Expand Up @@ -177,6 +180,7 @@
4B57105B28D0AC7F00AA053C /* ContentView.swift in Sources */,
4BB403CD2C19F9A80033B5E7 /* List.swift in Sources */,
4B094D622D65B82500E4C4A9 /* ShrinkDemo.swift in Sources */,
4B094D642D65B82700E4C4A9 /* OptionalImageDemo.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
4 changes: 4 additions & 0 deletions Development/AsyncMultiplexImage-Demo/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ struct ContentView: View {
NavigationLink("Shrink", destination: {
BookShrink()
})

NavigationLink("Optional Image", destination: {
OptionalImageDemo()
})
}
.navigationTitle("Multiplex Image")
}
Expand Down
149 changes: 149 additions & 0 deletions Development/AsyncMultiplexImage-Demo/OptionalImageDemo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import AsyncMultiplexImage
import AsyncMultiplexImage_Nuke
import Nuke
import SwiftUI

struct OptionalImageDemo: View, PreviewProvider {
var body: some View {
ContentView()
}

static var previews: some View {
Self()
.previewDisplayName("Optional Image Demo")
}

private struct ContentView: View {

@State private var imageRepresentation: ImageRepresentation? = nil

private let imageURLs = [
"https://images.unsplash.com/photo-1660668377331-da480e5339a0",
"https://images.unsplash.com/photo-1658214764191-b002b517e9e5",
"https://images.unsplash.com/photo-1587126396803-be14d33e49cf",
]

var body: some View {
VStack(spacing: 20) {

// Current state indicator
Text("Current state: \(stateDescription)")
.font(.caption)
.foregroundColor(.secondary)
.padding(.top)

// Image view
AsyncMultiplexImage(
imageRepresentation: imageRepresentation,
downloader: _SlowDownloader(pipeline: .shared),
content: PlaceholderContent()
)
.frame(height: 400)
.clipped()

// Control buttons
VStack(spacing: 12) {
HStack(spacing: 12) {
ForEach(0..<imageURLs.count, id: \.self) { index in
Button("Load Image \(index + 1)") {
loadImage(index: index)
}
.buttonStyle(.bordered)
}
}

Button("Clear") {
clearImage()
}
.buttonStyle(.borderedProminent)
.tint(.red)
}
.padding(.horizontal)

Spacer()
}
.navigationTitle("Optional Image Demo")
}

private var stateDescription: String {
if let imageRepresentation {
switch imageRepresentation {
case .remote(let multiplexImage):
return "some(remote: \(multiplexImage.identifier.prefix(30))...)"
case .loaded:
return "some(loaded)"
}
} else {
return "nil"
}
}

private func loadImage(index: Int) {
let urlString = imageURLs[index]
imageRepresentation = .remote(
MultiplexImage(
identifier: urlString,
urls: buildURLs(urlString)
)
)
}

private func clearImage() {
imageRepresentation = nil
}
}

// Custom content with placeholder
private struct PlaceholderContent: AsyncMultiplexImageContent {

func body(phase: AsyncMultiplexImagePhase) -> some View {
switch phase {
case .empty:
ZStack {
Color.gray.opacity(0.2)

VStack(spacing: 12) {
Image(systemName: "photo")
.font(.system(size: 60))
.foregroundColor(.gray)

Text("No Image (Placeholder)")
.font(.headline)
.foregroundColor(.secondary)

Text("Tap 'Load Image' to display an image")
.font(.caption)
.foregroundColor(.secondary)
}
}

case .progress(let image, _):
image
.resizable()
.scaledToFill()
.transition(.opacity.animation(.easeInOut))

case .success(let image, _):
image
.resizable()
.scaledToFill()
.transition(.opacity.animation(.easeInOut))

case .failure:
ZStack {
Color.red.opacity(0.2)

VStack(spacing: 12) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 60))
.foregroundColor(.red)

Text("Failed to load image")
.font(.headline)
.foregroundColor(.secondary)
}
}
}
}
}
}
40 changes: 25 additions & 15 deletions Sources/AsyncMultiplexImage/AsyncMultiplexImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ public enum ImageRepresentation: Equatable {
public struct AsyncMultiplexImage<
Content: AsyncMultiplexImageContent, Downloader: AsyncMultiplexImageDownloader
>: View {
private let imageRepresentation: ImageRepresentation

private let imageRepresentation: ImageRepresentation?
private let downloader: Downloader
private let content: Content

Expand Down Expand Up @@ -165,13 +165,13 @@ public struct AsyncMultiplexImage<
/// Creates an async multiplex image view with an image representation.
///
/// - Parameters:
/// - imageRepresentation: The image representation to display (either remote or pre-loaded).
/// - imageRepresentation: The image representation to display (either remote or pre-loaded). If nil, displays empty/placeholder state.
/// - downloader: The downloader actor to use for fetching images.
/// - clearsContentBeforeDownload: Whether to clear the content before starting a new download. Defaults to `true`.
/// - skipsFinalImageReload: Whether to skip reloading when a final image is already loaded for the same representation. When `false` (default), images reload when display size or representation changes. Defaults to `false`.
/// - content: A closure that builds the view content based on the current loading phase.
public init(
imageRepresentation: ImageRepresentation,
imageRepresentation: ImageRepresentation?,
downloader: Downloader,
clearsContentBeforeDownload: Bool = true,
skipsFinalImageReload: Bool = false,
Expand Down Expand Up @@ -201,18 +201,18 @@ public struct AsyncMultiplexImage<
private struct _AsyncMultiplexImage<
Content: AsyncMultiplexImageContent, Downloader: AsyncMultiplexImageDownloader
>: View {

private struct UpdateTrigger: Equatable {
let size: CGSize
let image: ImageRepresentation
let image: ImageRepresentation?
}

@State private var item: ResultContainer.ItemSwiftUI?

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

private let imageRepresentation: ImageRepresentation
private let imageRepresentation: ImageRepresentation?
private let downloader: Downloader
private let content: Content
private let clearsContentBeforeDownload: Bool
Expand All @@ -221,7 +221,7 @@ private struct _AsyncMultiplexImage<
public init(
clearsContentBeforeDownload: Bool,
skipsFinalImageReload: Bool,
imageRepresentation: ImageRepresentation,
imageRepresentation: ImageRepresentation?,
downloader: Downloader,
content: Content
) {
Expand Down Expand Up @@ -270,6 +270,16 @@ private struct _AsyncMultiplexImage<
),
{

// Handle nil imageRepresentation (placeholder state)
guard let imageRepresentation else {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
self.item = nil
}
return
}

if skipsFinalImageReload {
if let item,
case .final = item.phase,
Expand All @@ -279,22 +289,22 @@ private struct _AsyncMultiplexImage<
}
}

await withTaskCancellationHandler {
await withTaskCancellationHandler {

let newSize = displaySize

guard newSize.height > 0 && newSize.width > 0 else {
return
}

if clearsContentBeforeDownload {
var transaction = Transaction()
transaction.disablesAnimations = true
withTransaction(transaction) {
self.item = nil
}
}

switch imageRepresentation {
case .remote(let multiplexImage):

Expand Down