Skip to content

Commit bc122ff

Browse files
author
Christian Elies
committed
added support for phAssets; removed unnecessary macCatalyst workaround;
1 parent 9eb8540 commit bc122ff

File tree

9 files changed

+190
-28
lines changed

9 files changed

+190
-28
lines changed

Sources/RemoteImage/private/Models/RemoteImageState.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import Foundation
1010

1111
enum RemoteImageState {
1212
case error(_ error: Error)
13-
case image(_ image: RemoteImageType)
13+
case image(_ image: PlatformSpecificImageType)
1414
case loading
1515
}
1616

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import Photos
2+
3+
protocol PhotoKitServiceProvider {
4+
var photoKitService: PhotoKitServiceProtocol { get }
5+
}
6+
7+
protocol PhotoKitServiceProtocol {
8+
func getPhotoData(localIdentifier: String,
9+
success: @escaping (Data) -> Void,
10+
failure: @escaping (Error) -> Void)
11+
}
12+
13+
final class PhotoKitService {
14+
15+
}
16+
17+
extension PhotoKitService: PhotoKitServiceProtocol {
18+
func getPhotoData(localIdentifier: String,
19+
success: @escaping (Data) -> Void,
20+
failure: @escaping (Error) -> Void) {
21+
let fetchAssetsResult = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil)
22+
guard let phAsset = fetchAssetsResult.firstObject else {
23+
failure(PhotoKitServiceError.phAssetNotFound(localIdentifier: localIdentifier))
24+
return
25+
}
26+
27+
let options = PHImageRequestOptions()
28+
options.isNetworkAccessAllowed = true
29+
PHImageManager.default().requestImageDataAndOrientation(for: phAsset,
30+
options: options,
31+
resultHandler: { data, _, _, info in
32+
if let error = info?[PHImageErrorKey] as? Error {
33+
failure(error)
34+
} else if let data = data {
35+
success(data)
36+
} else {
37+
failure(PhotoKitServiceError.missingData)
38+
}
39+
})
40+
}
41+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// RemoteImageServiceDependencies.swift
3+
//
4+
//
5+
// Created by Christian Elies on 29.10.19.
6+
//
7+
8+
import Foundation
9+
10+
protocol RemoteImageServiceDependenciesProtocol: PhotoKitServiceProvider {
11+
12+
}
13+
14+
struct RemoteImageServiceDependencies: RemoteImageServiceDependenciesProtocol {
15+
let photoKitService: PhotoKitServiceProtocol
16+
17+
init() {
18+
photoKitService = PhotoKitService()
19+
}
20+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import Foundation
2+
3+
public enum PhotoKitServiceError: Error {
4+
case missingData
5+
case phAssetNotFound(localIdentifier: String)
6+
}
7+
8+
extension PhotoKitServiceError: LocalizedError {
9+
public var errorDescription: String? {
10+
switch self {
11+
case .missingData:
12+
return "The asset could not be loaded."
13+
case .phAssetNotFound(let localIdentifier):
14+
return "A PHAsset with the identifier \(localIdentifier) was not found."
15+
}
16+
}
17+
18+
public var failureReason: String? {
19+
switch self {
20+
case .missingData:
21+
return "The asset data could not be fetched. Maybe you are not connected to the internet."
22+
case .phAssetNotFound(let localIdentifier):
23+
return "An asset with the identifier \(localIdentifier) doesn't exist anymore."
24+
}
25+
}
26+
27+
public var recoverySuggestion: String? {
28+
switch self {
29+
case .missingData:
30+
return "Check your internet connection or try again later."
31+
default:
32+
return nil
33+
}
34+
}
35+
}
36+
37+
extension PhotoKitServiceError: CustomNSError {
38+
public static var errorDomain: String { String(describing: PhotoKitService.self) }
39+
40+
public var errorCode: Int {
41+
switch self {
42+
case .missingData:
43+
return 0
44+
case .phAssetNotFound:
45+
return 1
46+
}
47+
}
48+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//
2+
// PlatformSpecificImageType.swift
3+
//
4+
//
5+
// Created by Christian Elies on 08.09.19.
6+
//
7+
8+
import Foundation
9+
10+
#if canImport(UIKit)
11+
import UIKit
12+
public typealias PlatformSpecificImageType = UIImage
13+
14+
#elseif os(macOS)
15+
import AppKit
16+
public typealias PlatformSpecificImageType = NSImage
17+
#endif

Sources/RemoteImage/public/Models/RemoteImageType.swift

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,12 @@
22
// RemoteImageType.swift
33
//
44
//
5-
// Created by Christian Elies on 08.09.19.
5+
// Created by Christian Elies on 29.10.19.
66
//
77

88
import Foundation
99

10-
#if canImport(UIKit)
11-
import UIKit
12-
public typealias RemoteImageType = UIImage
13-
#elseif os(macOS)
14-
import AppKit
15-
public typealias RemoteImageType = NSImage
16-
#endif
10+
public enum RemoteImageType {
11+
case phAsset(localIdentifier: String)
12+
case url(_ url: URL)
13+
}

Sources/RemoteImage/public/Services/RemoteImageService.swift

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,29 @@ import Combine
1010
import Foundation
1111

1212
public final class RemoteImageService: NSObject, ObservableObject {
13+
private let dependencies: RemoteImageServiceDependenciesProtocol
1314
private var cancellable: AnyCancellable?
1415

15-
var state: RemoteImageState = .loading {
16-
didSet {
17-
objectWillChange.send()
18-
}
19-
}
16+
@Published var state: RemoteImageState = .loading
2017

21-
public static let cache = NSCache<NSURL, RemoteImageType>()
18+
public static let cache = NSCache<NSObject, PlatformSpecificImageType>()
2219

23-
public let objectWillChange = PassthroughSubject<Void, Never>()
20+
init(dependencies: RemoteImageServiceDependenciesProtocol) {
21+
self.dependencies = dependencies
22+
}
2423

25-
func fetchImage(atURL url: URL) {
24+
func fetchImage(ofType type: RemoteImageType) {
25+
switch type {
26+
case .url(let url):
27+
fetchImage(atURL: url)
28+
case .phAsset(let localIdentifier):
29+
fetchImage(withLocalIdentifier: localIdentifier)
30+
}
31+
}
32+
}
33+
34+
extension RemoteImageService {
35+
private func fetchImage(atURL url: URL) {
2636
cancellable?.cancel()
2737

2838
if let image = RemoteImageService.cache.object(forKey: url as NSURL) {
@@ -34,7 +44,7 @@ public final class RemoteImageService: NSObject, ObservableObject {
3444
let urlRequest = URLRequest(url: url)
3545

3646
cancellable = urlSession.dataTaskPublisher(for: urlRequest)
37-
.map { RemoteImageType(data: $0.data) }
47+
.map { PlatformSpecificImageType(data: $0.data) }
3848
.receive(on: RunLoop.main)
3949
.sink(receiveCompletion: { completion in
4050
switch completion {
@@ -51,4 +61,22 @@ public final class RemoteImageService: NSObject, ObservableObject {
5161
}
5262
}
5363
}
64+
65+
private func fetchImage(withLocalIdentifier localIdentifier: String) {
66+
if let image = RemoteImageService.cache.object(forKey: localIdentifier as NSString) {
67+
state = .image(image)
68+
return
69+
}
70+
71+
dependencies.photoKitService.getPhotoData(localIdentifier: localIdentifier, success: { data in
72+
if let image = PlatformSpecificImageType(data: data) {
73+
RemoteImageService.cache.setObject(image, forKey: localIdentifier as NSString)
74+
self.state = .image(image)
75+
} else {
76+
self.state = .error(RemoteImageServiceError.couldNotCreateImage)
77+
}
78+
}) { error in
79+
self.state = .error(error)
80+
}
81+
}
5482
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// RemoteImageServiceFactory.swift
3+
//
4+
//
5+
// Created by Christian Elies on 29.10.19.
6+
//
7+
8+
import Foundation
9+
10+
public final class RemoteImageServiceFactory {
11+
public static func makeRemoteImageService() -> RemoteImageService {
12+
let dependencies = RemoteImageServiceDependencies()
13+
return RemoteImageService(dependencies: dependencies)
14+
}
15+
}

Sources/RemoteImage/public/Views/RemoteImage.swift

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,12 @@ import Combine
1010
import SwiftUI
1111

1212
public struct RemoteImage<ErrorView: View, ImageView: View, LoadingView: View>: View {
13-
private let url: URL
13+
private let type: RemoteImageType
1414
private let errorView: (Error) -> ErrorView
1515
private let imageView: (Image) -> ImageView
1616
private let loadingView: () -> LoadingView
1717

18-
#if !targetEnvironment(macCatalyst)
19-
@ObservedObject private var service: RemoteImageService = RemoteImageService()
20-
#else
21-
@EnvironmentObject var service: RemoteImageService
22-
#endif
18+
@ObservedObject private var service = RemoteImageServiceFactory.makeRemoteImageService()
2319

2420
public var body: AnyView {
2521
switch service.state {
@@ -45,14 +41,14 @@ public struct RemoteImage<ErrorView: View, ImageView: View, LoadingView: View>:
4541
return AnyView(
4642
loadingView()
4743
.onAppear {
48-
self.service.fetchImage(atURL: self.url)
44+
self.service.fetchImage(ofType: self.type)
4945
}
5046
)
5147
}
5248
}
5349

54-
public init(url: URL, @ViewBuilder errorView: @escaping (Error) -> ErrorView, @ViewBuilder imageView: @escaping (Image) -> ImageView, @ViewBuilder loadingView: @escaping () -> LoadingView) {
55-
self.url = url
50+
public init(type: RemoteImageType, @ViewBuilder errorView: @escaping (Error) -> ErrorView, @ViewBuilder imageView: @escaping (Image) -> ImageView, @ViewBuilder loadingView: @escaping () -> LoadingView) {
51+
self.type = type
5652
self.errorView = errorView
5753
self.imageView = imageView
5854
self.loadingView = loadingView
@@ -63,7 +59,7 @@ public struct RemoteImage<ErrorView: View, ImageView: View, LoadingView: View>:
6359
struct RemoteImage_Previews: PreviewProvider {
6460
static var previews: some View {
6561
let url = URL(string: "https://images.unsplash.com/photo-1524419986249-348e8fa6ad4a?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1950&q=80")!
66-
return RemoteImage(url: url, errorView: { error in
62+
return RemoteImage(type: .url(url), errorView: { error in
6763
Text(error.localizedDescription)
6864
}, imageView: { image in
6965
image

0 commit comments

Comments
 (0)