Skip to content

Commit 7de2045

Browse files
author
Chris
authored
Merge pull request #18 from crelies/dev
4.0.0
2 parents 0a77464 + 868644b commit 7de2045

22 files changed

+155
-170
lines changed

Sources/RemoteImage/private/Models/RemoteImageState.swift

-31
This file was deleted.

Sources/RemoteImage/private/Protocols/RemoteImageServiceProtocol.swift

-16
This file was deleted.

Sources/RemoteImage/private/Protocols/RemoteImageURLDataPublisher.swift

-13
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// DefaultRemoteImageServiceDependencies.swift
3+
// RemoteImage
4+
//
5+
// Created by Christian Elies on 29.10.19.
6+
//
7+
8+
import Foundation
9+
10+
protocol DefaultRemoteImageServiceDependenciesProtocol: PhotoKitServiceProvider, RemoteImageURLDataPublisherProvider {
11+
12+
}
13+
14+
struct DefaultRemoteImageServiceDependencies: DefaultRemoteImageServiceDependenciesProtocol {
15+
let photoKitService: PhotoKitServiceProtocol
16+
let remoteImageURLDataPublisher: RemoteImageURLDataPublisher
17+
18+
init(remoteImageURLDataPublisher: RemoteImageURLDataPublisher) {
19+
photoKitService = PhotoKitService()
20+
self.remoteImageURLDataPublisher = remoteImageURLDataPublisher
21+
}
22+
}

Sources/RemoteImage/private/Services/PhotoKitService.swift

+8-4
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ protocol PhotoKitServiceProvider {
1212
}
1313

1414
protocol PhotoKitServiceProtocol {
15-
func getPhotoData(localIdentifier: String,
16-
_ completion: @escaping (Result<Data, Error>) -> Void)
15+
func getPhotoData(
16+
localIdentifier: String,
17+
_ completion: @escaping (Result<Data, Error>) -> Void
18+
)
1719
}
1820

1921
final class PhotoKitService {
@@ -22,8 +24,10 @@ final class PhotoKitService {
2224
}
2325

2426
extension PhotoKitService: PhotoKitServiceProtocol {
25-
func getPhotoData(localIdentifier: String,
26-
_ completion: @escaping (Result<Data, Error>) -> Void) {
27+
func getPhotoData(
28+
localIdentifier: String,
29+
_ completion: @escaping (Result<Data, Error>) -> Void
30+
) {
2731
let fetchAssetsResult = Self.asset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil)
2832
guard let phAsset = fetchAssetsResult.firstObject else {
2933
completion(.failure(PhotoKitServiceError.phAssetNotFound(localIdentifier: localIdentifier)))

Sources/RemoteImage/private/Services/RemoteImageServiceDependencies.swift

-22
This file was deleted.

Sources/RemoteImage/private/Extensions/URLSession+RemoteImageURLDataPublisher.swift renamed to Sources/RemoteImage/public/Extensions/URLSession+RemoteImageURLDataPublisher.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Combine
99
import Foundation
1010

1111
extension URLSession: RemoteImageURLDataPublisher {
12-
func dataPublisher(for request: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> {
13-
dataTaskPublisher(for: request).eraseToAnyPublisher()
12+
public func dataPublisher(for url: URL) -> AnyPublisher<(data: Data, response: URLResponse), URLError> {
13+
dataTaskPublisher(for: url).eraseToAnyPublisher()
1414
}
1515
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// RemoteImageState.swift
3+
// RemoteImage
4+
//
5+
// Created by Christian Elies on 11.08.19.
6+
// Copyright © 2019 Christian Elies. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
public enum RemoteImageState: Hashable {
12+
case error(_ error: NSError)
13+
case image(_ image: UniversalImage)
14+
case loading
15+
}

Sources/RemoteImage/public/Models/RemoteImageType.swift

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import Foundation
99

1010
public enum RemoteImageType {
11+
@available(*, deprecated, message: "Will be removed in the future because the localIdentifier is device specific and therefore cannot be used to uniquely identify a PHAsset across devices.")
1112
case phAsset(localIdentifier: String)
1213
case url(_ url: URL)
1314
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//
2+
// RemoteImageService.swift
3+
// RemoteImage
4+
//
5+
// Created by Christian Elies on 15.12.19.
6+
//
7+
8+
import Combine
9+
10+
public typealias RemoteImageCacheKeyProvider = (RemoteImageType) -> AnyObject
11+
12+
/// Represents the service associated with a `RemoteImage` view. Responsible for fetching the image and managing the state.
13+
public protocol RemoteImageService where Self: ObservableObject {
14+
/// The cache for the images fetched by any instance of `RemoteImageService`.
15+
static var cache: RemoteImageCache { get set }
16+
/// Provides a key for a given `RemoteImageType` used for storing an image in the cache.
17+
static var cacheKeyProvider: RemoteImageCacheKeyProvider { get set }
18+
19+
/// The current state of the image fetching process - `loading`, `error` or `image (success)`.
20+
var state: RemoteImageState { get set }
21+
22+
/// Fetches the image with the given type.
23+
///
24+
/// - Parameter type: Specifies the source type of the remote image. Choose between `.url` or `.phAsset`.
25+
func fetchImage(ofType type: RemoteImageType)
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//
2+
// RemoteImageURLDataPublisher.swift
3+
// RemoteImage
4+
//
5+
// Created by Christian Elies on 15.12.19.
6+
//
7+
8+
import Combine
9+
import Foundation
10+
11+
public protocol RemoteImageURLDataPublisher {
12+
func dataPublisher(for url: URL) -> AnyPublisher<(data: Data, response: URLResponse), URLError>
13+
}

Sources/RemoteImage/public/Services/RemoteImageService.swift renamed to Sources/RemoteImage/public/Services/DefaultRemoteImageService.swift

+8-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// RemoteImageService.swift
2+
// DefaultRemoteImageService.swift
33
// RemoteImage
44
//
55
// Created by Christian Elies on 11.08.19.
@@ -9,13 +9,11 @@
99
import Combine
1010
import Foundation
1111

12-
public typealias RemoteImageCacheKeyProvider = (RemoteImageType) -> AnyObject
13-
14-
public final class RemoteImageService: NSObject, ObservableObject, RemoteImageServiceProtocol {
15-
private let dependencies: RemoteImageServiceDependenciesProtocol
12+
public final class DefaultRemoteImageService: RemoteImageService {
13+
private let dependencies: DefaultRemoteImageServiceDependenciesProtocol
1614
private var cancellable: AnyCancellable?
1715

18-
@Published var state: RemoteImageState = .loading
16+
@Published public var state: RemoteImageState = .loading
1917

2018
public static var cache: RemoteImageCache = DefaultRemoteImageCache()
2119
public static var cacheKeyProvider: RemoteImageCacheKeyProvider = { remoteImageType in
@@ -25,11 +23,11 @@ public final class RemoteImageService: NSObject, ObservableObject, RemoteImageSe
2523
}
2624
}
2725

28-
init(dependencies: RemoteImageServiceDependenciesProtocol) {
26+
init(dependencies: DefaultRemoteImageServiceDependenciesProtocol) {
2927
self.dependencies = dependencies
3028
}
3129

32-
func fetchImage(ofType type: RemoteImageType) {
30+
public func fetchImage(ofType type: RemoteImageType) {
3331
switch type {
3432
case .url(let url):
3533
fetchImage(atURL: url)
@@ -39,7 +37,7 @@ public final class RemoteImageService: NSObject, ObservableObject, RemoteImageSe
3937
}
4038
}
4139

42-
private extension RemoteImageService {
40+
private extension DefaultRemoteImageService {
4341
func fetchImage(atURL url: URL) {
4442
cancellable?.cancel()
4543

@@ -49,9 +47,7 @@ private extension RemoteImageService {
4947
return
5048
}
5149

52-
let urlRequest = URLRequest(url: url)
53-
54-
cancellable = dependencies.remoteImageURLDataPublisher.dataPublisher(for: urlRequest)
50+
cancellable = dependencies.remoteImageURLDataPublisher.dataPublisher(for: url)
5551
.map { UniversalImage(data: $0.data) }
5652
.receive(on: RunLoop.main)
5753
.sink(receiveCompletion: { [weak self] completion in
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// DefaultRemoteImageServiceFactory.swift
3+
// RemoteImage
4+
//
5+
// Created by Christian Elies on 29.10.19.
6+
//
7+
8+
import Foundation
9+
10+
public final class DefaultRemoteImageServiceFactory {
11+
public static func makeDefaultRemoteImageService(remoteImageURLDataPublisher: RemoteImageURLDataPublisher = URLSession.shared) -> DefaultRemoteImageService {
12+
let dependencies = DefaultRemoteImageServiceDependencies(remoteImageURLDataPublisher: remoteImageURLDataPublisher)
13+
return DefaultRemoteImageService(dependencies: dependencies)
14+
}
15+
}

Sources/RemoteImage/public/Services/RemoteImageServiceFactory.swift

-13
This file was deleted.

Sources/RemoteImage/public/Views/RemoteImage.swift

+35-3
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
import Combine
1111
import SwiftUI
1212

13-
public struct RemoteImage<ErrorView: View, ImageView: View, LoadingView: View>: View {
13+
/// A custom Image view for remote images with support for a loading and error state.
14+
public struct RemoteImage<ErrorView: View, ImageView: View, LoadingView: View, Service: RemoteImageService>: View {
1415
private let type: RemoteImageType
1516
private let errorView: (Error) -> ErrorView
1617
private let imageView: (Image) -> ImageView
1718
private let loadingView: () -> LoadingView
1819

19-
@ObservedObject private var service = RemoteImageServiceFactory.makeRemoteImageService()
20+
@ObservedObject private var service: Service
2021

2122
public var body: some View {
2223
switch service.state {
@@ -33,11 +34,42 @@ public struct RemoteImage<ErrorView: View, ImageView: View, LoadingView: View>:
3334
}
3435
}
3536

36-
public init(type: RemoteImageType, @ViewBuilder errorView: @escaping (Error) -> ErrorView, @ViewBuilder imageView: @escaping (Image) -> ImageView, @ViewBuilder loadingView: @escaping () -> LoadingView) {
37+
/// Initializes the view with the given values, especially with a custom `RemoteImageService`.
38+
///
39+
/// - Parameters:
40+
/// - type: Specifies the source type of the remote image. Choose between `.url` or `.phAsset`.
41+
/// - service: An object conforming to the `RemoteImageService` protocol. Responsible for fetching the image and managing the state.
42+
/// - errorView: A view builder used to create the view displayed in the error state.
43+
/// - imageView: A view builder used to create the `Image` displayed in the image state.
44+
/// - loadingView: A view builder used to create the view displayed in the loading state.
45+
public init(type: RemoteImageType, service: Service, @ViewBuilder errorView: @escaping (Error) -> ErrorView, @ViewBuilder imageView: @escaping (Image) -> ImageView, @ViewBuilder loadingView: @escaping () -> LoadingView) {
3746
self.type = type
3847
self.errorView = errorView
3948
self.imageView = imageView
4049
self.loadingView = loadingView
50+
_service = ObservedObject(wrappedValue: service)
51+
52+
service.fetchImage(ofType: type)
53+
}
54+
}
55+
56+
extension RemoteImage where Service == DefaultRemoteImageService {
57+
/// Initializes the view with the given values. Uses the built-in `DefaultRemoteImageService`.
58+
///
59+
/// - Parameters:
60+
/// - type: Specifies the source type of the remote image. Choose between `.url` or `.phAsset`.
61+
/// - remoteImageURLDataPublisher: An object conforming to the `RemoteImageURLDataPublisher` protocol, by default `URLSession.shared` is used.
62+
/// - errorView: A view builder used to create the view displayed in the error state.
63+
/// - imageView: A view builder used to create the `Image` displayed in the image state.
64+
/// - loadingView: A view builder used to create the view displayed in the loading state.
65+
public init(type: RemoteImageType, remoteImageURLDataPublisher: RemoteImageURLDataPublisher = URLSession.shared, @ViewBuilder errorView: @escaping (Error) -> ErrorView, @ViewBuilder imageView: @escaping (Image) -> ImageView, @ViewBuilder loadingView: @escaping () -> LoadingView) {
66+
self.type = type
67+
self.errorView = errorView
68+
self.imageView = imageView
69+
self.loadingView = loadingView
70+
71+
let service = DefaultRemoteImageServiceFactory.makeDefaultRemoteImageService(remoteImageURLDataPublisher: remoteImageURLDataPublisher)
72+
_service = ObservedObject(wrappedValue: service)
4173

4274
service.fetchImage(ofType: type)
4375
}

Tests/RemoteImageTests/Extensions/URLSession+RemoteImageURLDataPublisherTests.swift

+2-3
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,8 @@ final class URLSession_RemoteImageURLDataPublisherTests: XCTestCase {
1616
return
1717
}
1818
let urlSession: URLSession = .shared
19-
let urlRequest = URLRequest(url: url)
20-
let dataTaskPublisher = urlSession.dataTaskPublisher(for: urlRequest).eraseToAnyPublisher()
21-
let dataPublisher = urlSession.dataPublisher(for: urlRequest)
19+
let dataTaskPublisher = urlSession.dataTaskPublisher(for: url).eraseToAnyPublisher()
20+
let dataPublisher = urlSession.dataPublisher(for: url)
2221
XCTAssertEqual(dataPublisher.description, dataTaskPublisher.description)
2322
}
2423

Tests/RemoteImageTests/Mocks/MockRemoteImageServiceDependencies.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
@testable import RemoteImage
99

10-
struct MockRemoteImageServiceDependencies: RemoteImageServiceDependenciesProtocol {
10+
struct MockRemoteImageServiceDependencies: DefaultRemoteImageServiceDependenciesProtocol {
1111
let photoKitService: PhotoKitServiceProtocol
1212
let remoteImageURLDataPublisher: RemoteImageURLDataPublisher
1313

Tests/RemoteImageTests/Mocks/MockRemoteImageURLDataPublisher.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Foundation
1212
final class MockRemoteImageURLDataPublisher: RemoteImageURLDataPublisher {
1313
var publisher = PassthroughSubject<(data: Data, response: URLResponse), URLError>()
1414

15-
func dataPublisher(for request: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> {
15+
func dataPublisher(for url: URL) -> AnyPublisher<(data: Data, response: URLResponse), URLError> {
1616
publisher.eraseToAnyPublisher()
1717
}
1818
}

0 commit comments

Comments
 (0)