diff --git a/Package.swift b/Package.swift index 6af4060..f761b58 100644 --- a/Package.swift +++ b/Package.swift @@ -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 diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift new file mode 100644 index 0000000..5adaa30 --- /dev/null +++ b/Package@swift-6.0.swift @@ -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"] + ), + ] +) diff --git a/Sources/NetworkImage/ImageSource.swift b/Sources/NetworkImage/ImageSource.swift index 150cf69..17c4f46 100644 --- a/Sources/NetworkImage/ImageSource.swift +++ b/Sources/NetworkImage/ImageSource.swift @@ -1,6 +1,6 @@ import Foundation -struct ImageSource: Hashable { +struct ImageSource: Hashable, Sendable { let url: URL let scale: CGFloat } diff --git a/Sources/NetworkImage/NetworkImage.swift b/Sources/NetworkImage/NetworkImage.swift index 0e1ad52..ba46e47 100644 --- a/Sources/NetworkImage/NetworkImage.swift +++ b/Sources/NetworkImage/NetworkImage.swift @@ -79,10 +79,6 @@ public struct NetworkImage: 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. /// @@ -168,15 +164,19 @@ public struct NetworkImage: 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) } ) } diff --git a/Sources/NetworkImage/NetworkImageCache.swift b/Sources/NetworkImage/NetworkImageCache.swift index b39ae12..3ac7d10 100644 --- a/Sources/NetworkImage/NetworkImageCache.swift +++ b/Sources/NetworkImage/NetworkImageCache.swift @@ -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? diff --git a/Sources/NetworkImage/NetworkImageModel.swift b/Sources/NetworkImage/NetworkImageModel.swift index a955280..7fb4a4f 100644 --- a/Sources/NetworkImage/NetworkImageModel.swift +++ b/Sources/NetworkImage/NetworkImageModel.swift @@ -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) } } }