Skip to content

Commit

Permalink
Merge pull request #18 from crelies/dev
Browse files Browse the repository at this point in the history
4.0.0
  • Loading branch information
Chris authored Nov 24, 2020
2 parents 0a77464 + 868644b commit 7de2045
Show file tree
Hide file tree
Showing 22 changed files with 155 additions and 170 deletions.
31 changes: 0 additions & 31 deletions Sources/RemoteImage/private/Models/RemoteImageState.swift

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// DefaultRemoteImageServiceDependencies.swift
// RemoteImage
//
// Created by Christian Elies on 29.10.19.
//

import Foundation

protocol DefaultRemoteImageServiceDependenciesProtocol: PhotoKitServiceProvider, RemoteImageURLDataPublisherProvider {

}

struct DefaultRemoteImageServiceDependencies: DefaultRemoteImageServiceDependenciesProtocol {
let photoKitService: PhotoKitServiceProtocol
let remoteImageURLDataPublisher: RemoteImageURLDataPublisher

init(remoteImageURLDataPublisher: RemoteImageURLDataPublisher) {
photoKitService = PhotoKitService()
self.remoteImageURLDataPublisher = remoteImageURLDataPublisher
}
}
12 changes: 8 additions & 4 deletions Sources/RemoteImage/private/Services/PhotoKitService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ protocol PhotoKitServiceProvider {
}

protocol PhotoKitServiceProtocol {
func getPhotoData(localIdentifier: String,
_ completion: @escaping (Result<Data, Error>) -> Void)
func getPhotoData(
localIdentifier: String,
_ completion: @escaping (Result<Data, Error>) -> Void
)
}

final class PhotoKitService {
Expand All @@ -22,8 +24,10 @@ final class PhotoKitService {
}

extension PhotoKitService: PhotoKitServiceProtocol {
func getPhotoData(localIdentifier: String,
_ completion: @escaping (Result<Data, Error>) -> Void) {
func getPhotoData(
localIdentifier: String,
_ completion: @escaping (Result<Data, Error>) -> Void
) {
let fetchAssetsResult = Self.asset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil)
guard let phAsset = fetchAssetsResult.firstObject else {
completion(.failure(PhotoKitServiceError.phAssetNotFound(localIdentifier: localIdentifier)))
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import Combine
import Foundation

extension URLSession: RemoteImageURLDataPublisher {
func dataPublisher(for request: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> {
dataTaskPublisher(for: request).eraseToAnyPublisher()
public func dataPublisher(for url: URL) -> AnyPublisher<(data: Data, response: URLResponse), URLError> {
dataTaskPublisher(for: url).eraseToAnyPublisher()
}
}
15 changes: 15 additions & 0 deletions Sources/RemoteImage/public/Models/RemoteImageState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// RemoteImageState.swift
// RemoteImage
//
// Created by Christian Elies on 11.08.19.
// Copyright © 2019 Christian Elies. All rights reserved.
//

import Foundation

public enum RemoteImageState: Hashable {
case error(_ error: NSError)
case image(_ image: UniversalImage)
case loading
}
1 change: 1 addition & 0 deletions Sources/RemoteImage/public/Models/RemoteImageType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Foundation

public enum RemoteImageType {
@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.")
case phAsset(localIdentifier: String)
case url(_ url: URL)
}
26 changes: 26 additions & 0 deletions Sources/RemoteImage/public/Protocols/RemoteImageService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// RemoteImageService.swift
// RemoteImage
//
// Created by Christian Elies on 15.12.19.
//

import Combine

public typealias RemoteImageCacheKeyProvider = (RemoteImageType) -> AnyObject

/// Represents the service associated with a `RemoteImage` view. Responsible for fetching the image and managing the state.
public protocol RemoteImageService where Self: ObservableObject {
/// The cache for the images fetched by any instance of `RemoteImageService`.
static var cache: RemoteImageCache { get set }
/// Provides a key for a given `RemoteImageType` used for storing an image in the cache.
static var cacheKeyProvider: RemoteImageCacheKeyProvider { get set }

/// The current state of the image fetching process - `loading`, `error` or `image (success)`.
var state: RemoteImageState { get set }

/// Fetches the image with the given type.
///
/// - Parameter type: Specifies the source type of the remote image. Choose between `.url` or `.phAsset`.
func fetchImage(ofType type: RemoteImageType)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// RemoteImageURLDataPublisher.swift
// RemoteImage
//
// Created by Christian Elies on 15.12.19.
//

import Combine
import Foundation

public protocol RemoteImageURLDataPublisher {
func dataPublisher(for url: URL) -> AnyPublisher<(data: Data, response: URLResponse), URLError>
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// RemoteImageService.swift
// DefaultRemoteImageService.swift
// RemoteImage
//
// Created by Christian Elies on 11.08.19.
Expand All @@ -9,13 +9,11 @@
import Combine
import Foundation

public typealias RemoteImageCacheKeyProvider = (RemoteImageType) -> AnyObject

public final class RemoteImageService: NSObject, ObservableObject, RemoteImageServiceProtocol {
private let dependencies: RemoteImageServiceDependenciesProtocol
public final class DefaultRemoteImageService: RemoteImageService {
private let dependencies: DefaultRemoteImageServiceDependenciesProtocol
private var cancellable: AnyCancellable?

@Published var state: RemoteImageState = .loading
@Published public var state: RemoteImageState = .loading

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

init(dependencies: RemoteImageServiceDependenciesProtocol) {
init(dependencies: DefaultRemoteImageServiceDependenciesProtocol) {
self.dependencies = dependencies
}

func fetchImage(ofType type: RemoteImageType) {
public func fetchImage(ofType type: RemoteImageType) {
switch type {
case .url(let url):
fetchImage(atURL: url)
Expand All @@ -39,7 +37,7 @@ public final class RemoteImageService: NSObject, ObservableObject, RemoteImageSe
}
}

private extension RemoteImageService {
private extension DefaultRemoteImageService {
func fetchImage(atURL url: URL) {
cancellable?.cancel()

Expand All @@ -49,9 +47,7 @@ private extension RemoteImageService {
return
}

let urlRequest = URLRequest(url: url)

cancellable = dependencies.remoteImageURLDataPublisher.dataPublisher(for: urlRequest)
cancellable = dependencies.remoteImageURLDataPublisher.dataPublisher(for: url)
.map { UniversalImage(data: $0.data) }
.receive(on: RunLoop.main)
.sink(receiveCompletion: { [weak self] completion in
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//
// DefaultRemoteImageServiceFactory.swift
// RemoteImage
//
// Created by Christian Elies on 29.10.19.
//

import Foundation

public final class DefaultRemoteImageServiceFactory {
public static func makeDefaultRemoteImageService(remoteImageURLDataPublisher: RemoteImageURLDataPublisher = URLSession.shared) -> DefaultRemoteImageService {
let dependencies = DefaultRemoteImageServiceDependencies(remoteImageURLDataPublisher: remoteImageURLDataPublisher)
return DefaultRemoteImageService(dependencies: dependencies)
}
}

This file was deleted.

38 changes: 35 additions & 3 deletions Sources/RemoteImage/public/Views/RemoteImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@
import Combine
import SwiftUI

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

@ObservedObject private var service = RemoteImageServiceFactory.makeRemoteImageService()
@ObservedObject private var service: Service

public var body: some View {
switch service.state {
Expand All @@ -33,11 +34,42 @@ public struct RemoteImage<ErrorView: View, ImageView: View, LoadingView: View>:
}
}

public init(type: RemoteImageType, @ViewBuilder errorView: @escaping (Error) -> ErrorView, @ViewBuilder imageView: @escaping (Image) -> ImageView, @ViewBuilder loadingView: @escaping () -> LoadingView) {
/// Initializes the view with the given values, especially with a custom `RemoteImageService`.
///
/// - Parameters:
/// - type: Specifies the source type of the remote image. Choose between `.url` or `.phAsset`.
/// - service: An object conforming to the `RemoteImageService` protocol. Responsible for fetching the image and managing the state.
/// - errorView: A view builder used to create the view displayed in the error state.
/// - imageView: A view builder used to create the `Image` displayed in the image state.
/// - loadingView: A view builder used to create the view displayed in the loading state.
public init(type: RemoteImageType, service: Service, @ViewBuilder errorView: @escaping (Error) -> ErrorView, @ViewBuilder imageView: @escaping (Image) -> ImageView, @ViewBuilder loadingView: @escaping () -> LoadingView) {
self.type = type
self.errorView = errorView
self.imageView = imageView
self.loadingView = loadingView
_service = ObservedObject(wrappedValue: service)

service.fetchImage(ofType: type)
}
}

extension RemoteImage where Service == DefaultRemoteImageService {
/// Initializes the view with the given values. Uses the built-in `DefaultRemoteImageService`.
///
/// - Parameters:
/// - type: Specifies the source type of the remote image. Choose between `.url` or `.phAsset`.
/// - remoteImageURLDataPublisher: An object conforming to the `RemoteImageURLDataPublisher` protocol, by default `URLSession.shared` is used.
/// - errorView: A view builder used to create the view displayed in the error state.
/// - imageView: A view builder used to create the `Image` displayed in the image state.
/// - loadingView: A view builder used to create the view displayed in the loading state.
public init(type: RemoteImageType, remoteImageURLDataPublisher: RemoteImageURLDataPublisher = URLSession.shared, @ViewBuilder errorView: @escaping (Error) -> ErrorView, @ViewBuilder imageView: @escaping (Image) -> ImageView, @ViewBuilder loadingView: @escaping () -> LoadingView) {
self.type = type
self.errorView = errorView
self.imageView = imageView
self.loadingView = loadingView

let service = DefaultRemoteImageServiceFactory.makeDefaultRemoteImageService(remoteImageURLDataPublisher: remoteImageURLDataPublisher)
_service = ObservedObject(wrappedValue: service)

service.fetchImage(ofType: type)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ final class URLSession_RemoteImageURLDataPublisherTests: XCTestCase {
return
}
let urlSession: URLSession = .shared
let urlRequest = URLRequest(url: url)
let dataTaskPublisher = urlSession.dataTaskPublisher(for: urlRequest).eraseToAnyPublisher()
let dataPublisher = urlSession.dataPublisher(for: urlRequest)
let dataTaskPublisher = urlSession.dataTaskPublisher(for: url).eraseToAnyPublisher()
let dataPublisher = urlSession.dataPublisher(for: url)
XCTAssertEqual(dataPublisher.description, dataTaskPublisher.description)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

@testable import RemoteImage

struct MockRemoteImageServiceDependencies: RemoteImageServiceDependenciesProtocol {
struct MockRemoteImageServiceDependencies: DefaultRemoteImageServiceDependenciesProtocol {
let photoKitService: PhotoKitServiceProtocol
let remoteImageURLDataPublisher: RemoteImageURLDataPublisher

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Foundation
final class MockRemoteImageURLDataPublisher: RemoteImageURLDataPublisher {
var publisher = PassthroughSubject<(data: Data, response: URLResponse), URLError>()

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

0 comments on commit 7de2045

Please sign in to comment.