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: 0 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// swift-tools-version:5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

Expand Down
24 changes: 24 additions & 0 deletions [email protected]
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// swift-tools-version:6.0

import PackageDescription

let package = Package(
name: "NetworkImage",
platforms: [
.macOS(.v11),
.iOS(.v14),
.tvOS(.v14),
.watchOS(.v7),
],
products: [
.library(name: "NetworkImage", targets: ["NetworkImage"])
],
dependencies: [],
targets: [
.target(name: "NetworkImage"),
.testTarget(
name: "NetworkImageTests",
dependencies: ["NetworkImage"]
),
]
)
2 changes: 1 addition & 1 deletion Sources/NetworkImage/ImageSource.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

struct ImageSource: Hashable {
struct ImageSource: Hashable, Sendable {
let url: URL
let scale: CGFloat
}
20 changes: 10 additions & 10 deletions Sources/NetworkImage/NetworkImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,6 @@ public struct NetworkImage<Content>: View where Content: View {
private let transaction: Transaction
private let content: (NetworkImageState) -> Content

private var environment: NetworkImageModel.Environment {
.init(transaction: self.transaction, imageLoader: self.imageLoader)
}

/// Loads and displays an image from the specified URL using
/// a default placeholder until the image loads.
///
Expand Down Expand Up @@ -168,15 +164,19 @@ public struct NetworkImage<Content>: View where Content: View {

public var body: some View {
if #available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) {
self.content(self.model.state.image)
.task(id: self.source) {
await self.model.onAppear(source: self.source, environment: self.environment)
self.content(model.image)
.task(id: source) {
model.imageLoaderChanged(imageLoader)
model.transactionChanged(transaction)
await model.sourceChanged(source)
}
} else {
self.content(self.model.state.image)
self.content(model.image)
.modifier(
TaskModifier(id: self.source) {
await self.model.onAppear(source: self.source, environment: self.environment)
TaskModifier(id: source) { @MainActor in
model.imageLoaderChanged(imageLoader)
model.transactionChanged(transaction)
await model.sourceChanged(source)
}
)
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/NetworkImage/NetworkImageCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import CoreGraphics
import Foundation

/// A type that temporarily stores images in memory, keyed by the URL from which they were loaded.
public protocol NetworkImageCache: AnyObject, Sendable {
public protocol NetworkImageCache: AnyObject {
/// Returns the image associated with a given URL.
func image(for url: URL) -> CGImage?

Expand Down
65 changes: 38 additions & 27 deletions Sources/NetworkImage/NetworkImageModel.swift
Original file line number Diff line number Diff line change
@@ -1,45 +1,56 @@
import SwiftUI

final class NetworkImageModel: ObservableObject {
struct Environment {
let transaction: Transaction
let imageLoader: NetworkImageLoader
}
@MainActor final class NetworkImageModel: ObservableObject {
@Published private(set) var source: ImageSource?
@Published private(set) var image: NetworkImageState = .empty

struct State: Equatable {
var source: ImageSource?
var image: NetworkImageState = .empty
}
private var transaction = Transaction()
private var imageLoader: NetworkImageLoader = DefaultNetworkImageLoader.shared

@Published private(set) var state: State = .init()
// MARK: Actions

@MainActor func onAppear(source: ImageSource?, environment: Environment) async {
guard source != self.state.source else { return }
func sourceChanged(_ source: ImageSource?) async {
guard source != self.source else { return }

guard let source else {
self.state = .init()
return
self.source = source

if let source {
await loadImage(source: source)
}
}

self.state.source = source
func transactionChanged(_ transaction: Transaction) {
self.transaction = transaction
}

let image: NetworkImageState
func imageLoaderChanged(_ imageLoader: NetworkImageLoader) {
self.imageLoader = imageLoader
}

do {
let cgImage = try await environment.imageLoader.image(from: source.url)
image = .success(
image: .init(decorative: cgImage, scale: source.scale),
private func loadImageFinished(_ cgImage: CGImage, scale: CGFloat) {
withTransaction(transaction) {
self.image = .success(
image: Image(decorative: cgImage, scale: scale),
idealSize: CGSize(
width: CGFloat(cgImage.width) / source.scale,
height: CGFloat(cgImage.height) / source.scale
width: CGFloat(cgImage.width) / scale,
height: CGFloat(cgImage.height) / scale
)
)
} catch {
image = .failure
}
}

private func loadImageFailed(_: Error) {
self.image = .failure
}

withTransaction(environment.transaction) {
self.state.image = image
// MARK: Effects

private func loadImage(source: ImageSource) async {
do {
let cgImage = try await imageLoader.image(from: source.url)
loadImageFinished(cgImage, scale: source.scale)
} catch {
loadImageFailed(error)
}
}
}
Loading