-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Restore platform requirements and bump dependencies
- Loading branch information
1 parent
d7b018e
commit d45f014
Showing
13 changed files
with
732 additions
and
757 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,118 +1,116 @@ | ||
#if canImport(Combine) | ||
import Combine | ||
import Foundation | ||
import XCTestDynamicOverlay | ||
import Combine | ||
import Foundation | ||
import XCTestDynamicOverlay | ||
|
||
/// Loads and caches images. | ||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
public struct NetworkImageLoader { | ||
private let _image: (URL) -> AnyPublisher<OSImage, Error> | ||
private let _cachedImage: (URL) -> OSImage? | ||
/// Loads and caches images. | ||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
public struct NetworkImageLoader { | ||
private let _image: (URL) -> AnyPublisher<OSImage, Error> | ||
private let _cachedImage: (URL) -> OSImage? | ||
|
||
/// Creates an image loader. | ||
/// - Parameters: | ||
/// - urlSession: The `URLSession` that will load the images. | ||
/// - imageCache: An immediate cache to store the images in memory. | ||
public init(urlSession: URLSession, imageCache: NetworkImageCache) { | ||
self.init(urlLoader: URLLoader(urlSession: urlSession), imageCache: imageCache) | ||
} | ||
/// Creates an image loader. | ||
/// - Parameters: | ||
/// - urlSession: The `URLSession` that will load the images. | ||
/// - imageCache: An immediate cache to store the images in memory. | ||
public init(urlSession: URLSession, imageCache: NetworkImageCache) { | ||
self.init(urlLoader: URLLoader(urlSession: urlSession), imageCache: imageCache) | ||
} | ||
|
||
init(urlLoader: URLLoader, imageCache: NetworkImageCache) { | ||
self.init( | ||
image: { url in | ||
if let image = imageCache.image(for: url) { | ||
return Just(image) | ||
.setFailureType(to: Error.self) | ||
.eraseToAnyPublisher() | ||
} else { | ||
return urlLoader.dataTaskPublisher(for: url) | ||
.tryMap { data, response in | ||
if let httpResponse = response as? HTTPURLResponse { | ||
guard 200 ..< 300 ~= httpResponse.statusCode else { | ||
throw NetworkImageError.badStatus(httpResponse.statusCode) | ||
} | ||
init(urlLoader: URLLoader, imageCache: NetworkImageCache) { | ||
self.init( | ||
image: { url in | ||
if let image = imageCache.image(for: url) { | ||
return Just(image) | ||
.setFailureType(to: Error.self) | ||
.eraseToAnyPublisher() | ||
} else { | ||
return urlLoader.dataTaskPublisher(for: url) | ||
.tryMap { data, response in | ||
if let httpResponse = response as? HTTPURLResponse { | ||
guard 200 ..< 300 ~= httpResponse.statusCode else { | ||
throw NetworkImageError.badStatus(httpResponse.statusCode) | ||
} | ||
|
||
return try decodeImage(from: data) | ||
} | ||
.handleEvents(receiveOutput: { image in | ||
imageCache.setImage(image, for: url) | ||
}) | ||
.eraseToAnyPublisher() | ||
} | ||
}, | ||
cachedImage: { url in | ||
imageCache.image(for: url) | ||
} | ||
) | ||
} | ||
|
||
init( | ||
image: @escaping (URL) -> AnyPublisher<OSImage, Error>, | ||
cachedImage: @escaping (URL) -> OSImage? | ||
) { | ||
_image = image | ||
_cachedImage = cachedImage | ||
} | ||
return try decodeImage(from: data) | ||
} | ||
.handleEvents(receiveOutput: { image in | ||
imageCache.setImage(image, for: url) | ||
}) | ||
.eraseToAnyPublisher() | ||
} | ||
}, | ||
cachedImage: { url in | ||
imageCache.image(for: url) | ||
} | ||
) | ||
} | ||
|
||
/// Returns a publisher that loads an image for a given URL. | ||
public func image(for url: URL) -> AnyPublisher<OSImage, Error> { | ||
_image(url) | ||
} | ||
init( | ||
image: @escaping (URL) -> AnyPublisher<OSImage, Error>, | ||
cachedImage: @escaping (URL) -> OSImage? | ||
) { | ||
_image = image | ||
_cachedImage = cachedImage | ||
} | ||
|
||
/// Returns the cached image for a given URL if there is any. | ||
public func cachedImage(for url: URL) -> OSImage? { | ||
_cachedImage(url) | ||
} | ||
/// Returns a publisher that loads an image for a given URL. | ||
public func image(for url: URL) -> AnyPublisher<OSImage, Error> { | ||
_image(url) | ||
} | ||
|
||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
public extension NetworkImageLoader { | ||
/// The shared singleton image loader. | ||
/// | ||
/// The shared image loader uses the shared `URLCache` and provides | ||
/// reasonable defaults for disk and memory caches. | ||
static let shared = Self(urlSession: .imageLoading, imageCache: NetworkImageCache()) | ||
/// Returns the cached image for a given URL if there is any. | ||
public func cachedImage(for url: URL) -> OSImage? { | ||
_cachedImage(url) | ||
} | ||
} | ||
|
||
#if DEBUG | ||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
public extension NetworkImageLoader { | ||
static func mock<P>( | ||
url matchingURL: URL, | ||
withResponse response: P | ||
) -> Self where P: Publisher, P.Output == OSImage, P.Failure == Error { | ||
Self { url in | ||
if url != matchingURL { | ||
XCTFail("\(Self.self).image recevied an unexpected URL: \(url)") | ||
} | ||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
public extension NetworkImageLoader { | ||
/// The shared singleton image loader. | ||
/// | ||
/// The shared image loader uses the shared `URLCache` and provides | ||
/// reasonable defaults for disk and memory caches. | ||
static let shared = Self(urlSession: .imageLoading, imageCache: NetworkImageCache()) | ||
} | ||
|
||
return response.eraseToAnyPublisher() | ||
} cachedImage: { _ in | ||
nil | ||
#if DEBUG | ||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
public extension NetworkImageLoader { | ||
static func mock<P>( | ||
url matchingURL: URL, | ||
withResponse response: P | ||
) -> Self where P: Publisher, P.Output == OSImage, P.Failure == Error { | ||
Self { url in | ||
if url != matchingURL { | ||
XCTFail("\(Self.self).image recevied an unexpected URL: \(url)") | ||
} | ||
|
||
return response.eraseToAnyPublisher() | ||
} cachedImage: { _ in | ||
nil | ||
} | ||
} | ||
|
||
static func mock<P>( | ||
response: P | ||
) -> Self where P: Publisher, P.Output == OSImage, P.Failure == Error { | ||
Self { _ in | ||
response.eraseToAnyPublisher() | ||
} cachedImage: { _ in | ||
nil | ||
} | ||
static func mock<P>( | ||
response: P | ||
) -> Self where P: Publisher, P.Output == OSImage, P.Failure == Error { | ||
Self { _ in | ||
response.eraseToAnyPublisher() | ||
} cachedImage: { _ in | ||
nil | ||
} | ||
} | ||
|
||
static var failing: Self { | ||
Self { _ in | ||
XCTFail("\(Self.self).image is unimplemented") | ||
return Just(OSImage()) | ||
.setFailureType(to: Error.self) | ||
.eraseToAnyPublisher() | ||
} cachedImage: { _ in | ||
nil | ||
} | ||
static var failing: Self { | ||
Self { _ in | ||
XCTFail("\(Self.self).image is unimplemented") | ||
return Just(OSImage()) | ||
.setFailureType(to: Error.self) | ||
.eraseToAnyPublisher() | ||
} cachedImage: { _ in | ||
nil | ||
} | ||
} | ||
#endif | ||
} | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,64 +1,62 @@ | ||
#if canImport(Combine) | ||
import Combine | ||
import CombineSchedulers | ||
import Foundation | ||
import Combine | ||
import CombineSchedulers | ||
import Foundation | ||
|
||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
internal struct NetworkImageEnvironment { | ||
var imageLoader: NetworkImageLoader | ||
var mainQueue: AnySchedulerOf<DispatchQueue> | ||
} | ||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
internal struct NetworkImageEnvironment { | ||
var imageLoader: NetworkImageLoader | ||
var mainQueue: AnySchedulerOf<DispatchQueue> | ||
} | ||
|
||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
internal final class NetworkImageStore: ObservableObject { | ||
enum Action { | ||
case onAppear(environment: NetworkImageEnvironment) | ||
case didLoadImage(OSImage) | ||
case didFail | ||
} | ||
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) | ||
internal final class NetworkImageStore: ObservableObject { | ||
enum Action { | ||
case onAppear(environment: NetworkImageEnvironment) | ||
case didLoadImage(OSImage) | ||
case didFail | ||
} | ||
|
||
enum State: Equatable { | ||
case notRequested(URL) | ||
case placeholder | ||
case image(OSImage) | ||
case fallback | ||
} | ||
enum State: Equatable { | ||
case notRequested(URL) | ||
case placeholder | ||
case image(OSImage) | ||
case fallback | ||
} | ||
|
||
@Published private(set) var state: State | ||
private var cancellables: Set<AnyCancellable> = [] | ||
@Published private(set) var state: State | ||
private var cancellables: Set<AnyCancellable> = [] | ||
|
||
init(url: URL?) { | ||
if let url = url { | ||
state = .notRequested(url) | ||
} else { | ||
state = .fallback | ||
} | ||
init(url: URL?) { | ||
if let url = url { | ||
state = .notRequested(url) | ||
} else { | ||
state = .fallback | ||
} | ||
} | ||
|
||
func send(_ action: Action) { | ||
switch action { | ||
case let .onAppear(environment): | ||
guard case let .notRequested(url) = state else { | ||
return | ||
} | ||
if let image = environment.imageLoader.cachedImage(for: url) { | ||
state = .image(image) | ||
} else { | ||
state = .placeholder | ||
environment.imageLoader.image(for: url) | ||
.map { .didLoadImage($0) } | ||
.replaceError(with: .didFail) | ||
.receive(on: environment.mainQueue) | ||
.sink { [weak self] action in | ||
self?.send(action) | ||
} | ||
.store(in: &cancellables) | ||
} | ||
case let .didLoadImage(image): | ||
func send(_ action: Action) { | ||
switch action { | ||
case let .onAppear(environment): | ||
guard case let .notRequested(url) = state else { | ||
return | ||
} | ||
if let image = environment.imageLoader.cachedImage(for: url) { | ||
state = .image(image) | ||
case .didFail: | ||
state = .fallback | ||
} else { | ||
state = .placeholder | ||
environment.imageLoader.image(for: url) | ||
.map { .didLoadImage($0) } | ||
.replaceError(with: .didFail) | ||
.receive(on: environment.mainQueue) | ||
.sink { [weak self] action in | ||
self?.send(action) | ||
} | ||
.store(in: &cancellables) | ||
} | ||
case let .didLoadImage(image): | ||
state = .image(image) | ||
case .didFail: | ||
state = .fallback | ||
} | ||
} | ||
#endif | ||
} |
Oops, something went wrong.