Skip to content

Commit

Permalink
Restore platform requirements and bump dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
gonzalezreal committed Aug 29, 2021
1 parent d7b018e commit d45f014
Show file tree
Hide file tree
Showing 13 changed files with 732 additions and 757 deletions.
12 changes: 6 additions & 6 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,26 @@
"repositoryURL": "https://github.com/pointfreeco/combine-schedulers",
"state": {
"branch": null,
"revision": "c37e5ae8012fb654af776cc556ff8ae64398c841",
"version": "0.5.0"
"revision": "6bde3b0063ba8e7537b43744948535ca7e9e0dad",
"version": "0.5.2"
}
},
{
"package": "SnapshotTesting",
"repositoryURL": "https://github.com/pointfreeco/swift-snapshot-testing",
"state": {
"branch": null,
"revision": "c466812aa2e22898f27557e2e780d3aad7a27203",
"version": "1.8.2"
"revision": "f8a9c997c3c1dab4e216a8ec9014e23144cbab37",
"version": "1.9.0"
}
},
{
"package": "xctest-dynamic-overlay",
"repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state": {
"branch": null,
"revision": "603974e3909ad4b48ba04aad7e0ceee4f077a518",
"version": "0.1.0"
"revision": "50a70a9d3583fe228ce672e8923010c8df2deddd",
"version": "0.2.1"
}
}
]
Expand Down
14 changes: 7 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ import PackageDescription
let package = Package(
name: "NetworkImage",
platforms: [
.macOS(.v10_12),
.iOS(.v11),
.tvOS(.v11),
.watchOS(.v3),
.macOS(.v10_15),
.iOS(.v13),
.tvOS(.v13),
.watchOS(.v6),
],
products: [
.library(name: "NetworkImage", targets: ["NetworkImage"]),
],
dependencies: [
.package(url: "https://github.com/pointfreeco/combine-schedulers", from: "0.5.0"),
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "0.1.0"),
.package(url: "https://github.com/pointfreeco/combine-schedulers", from: "0.5.2"),
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "0.2.1"),
.package(
name: "SnapshotTesting",
url: "https://github.com/pointfreeco/swift-snapshot-testing",
from: "1.8.2"
from: "1.9.0"
),
],
targets: [
Expand Down
192 changes: 95 additions & 97 deletions Sources/NetworkImage/Core/NetworkImageLoader.swift
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
104 changes: 51 additions & 53 deletions Sources/NetworkImage/Core/NetworkImageStore.swift
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
}
Loading

0 comments on commit d45f014

Please sign in to comment.