Skip to content

Commit 32a1740

Browse files
authored
Merge pull request #6 from bullinnyc/add-image-cache-property-wrapper
Add image cache property wrapper.
2 parents b153880 + 93efc8e commit 32a1740

11 files changed

Lines changed: 181 additions & 75 deletions

File tree

Examples/ContentView.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ struct ContentView: View {
2020
"https://image.tmdb.org/t/p/original/arw2vcBveWOVZr6pxd9XTd1TdQa.jpg"
2121
]
2222

23-
private static let paddingStandart: CGFloat = 20
23+
private static let standartPadding: CGFloat = 20
2424

2525
// MARK: - Body
2626

@@ -77,7 +77,7 @@ struct ContentView: View {
7777
}
7878
)
7979
.frame(
80-
maxWidth: size.width - Self.paddingStandart * 2,
80+
maxWidth: size.width - Self.standartPadding * 2,
8181
idealHeight:
8282
getIdealHeight(
8383
geometrySize: size,
@@ -86,10 +86,10 @@ struct ContentView: View {
8686
)
8787
.clipped()
8888
.clipShape(RoundedRectangle(cornerRadius: 20))
89-
.padding([.leading, .trailing], Self.paddingStandart)
89+
.padding([.leading, .trailing], Self.standartPadding)
9090
}
9191
}
92-
.padding([.top, .bottom], Self.paddingStandart)
92+
.padding([.top, .bottom], Self.standartPadding)
9393
}
9494
}
9595
}
@@ -102,7 +102,7 @@ struct ContentView: View {
102102

103103
init() {
104104
// Set image cache limit.
105-
TemporaryImageCache.shared.setCacheLimit(
105+
ImageCache().wrappedValue.setCacheLimit(
106106
countLimit: 1000, // 1000 items
107107
totalCostLimit: 1024 * 1024 * 200 // 200 MB
108108
)
@@ -114,7 +114,7 @@ struct ContentView: View {
114114
geometrySize: CGSize,
115115
aspectRatio: CGFloat
116116
) -> CGFloat {
117-
let width = geometrySize.width - Self.paddingStandart * 2
117+
let width = geometrySize.width - Self.standartPadding * 2
118118
return width / aspectRatio
119119
}
120120
}

README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,11 +97,23 @@ CachedAsyncImage(
9797
**Note:** The default value is `0`, e.g. is no count limit and is no total cost limit.
9898

9999
```swift
100-
// Set image cache limit.
101-
TemporaryImageCache.shared.setCacheLimit(
102-
countLimit: 1000, // 1000 items
103-
totalCostLimit: 1024 * 1024 * 200 // 200 MB
104-
)
100+
init() {
101+
// Set image cache limit.
102+
ImageCache().wrappedValue.setCacheLimit(
103+
countLimit: 1000, // 1000 items
104+
totalCostLimit: 1024 * 1024 * 200 // 200 MB
105+
)
106+
}
107+
```
108+
109+
### You can also read this value from within a view to access the image cache management
110+
111+
```swift
112+
struct MyView: View {
113+
@ImageCache private var imageCache
114+
115+
// ...
116+
}
105117
```
106118

107119
## Requirements
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// ImageCache.swift
3+
// CachedAsyncImage
4+
//
5+
// Created by Dmitry Kononchuk on 07.01.2024.
6+
// Copyright © 2024 Dmitry Kononchuk. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
/// A property wrapper type that reflects a value from `TemporaryImageCache`.
12+
///
13+
/// Read this value from within a view to access the image cache management.
14+
///
15+
/// struct MyView: View {
16+
/// @ImageCache private var imageCache
17+
///
18+
/// // ...
19+
/// }
20+
///
21+
@propertyWrapper
22+
public struct ImageCache {
23+
// MARK: - Public Properties
24+
25+
/// The wrapped value property provides primary access to the value’s data.
26+
public var wrappedValue: ImageCacheProtocol {
27+
get { storage.imageCache }
28+
nonmutating set { storage.imageCache = newValue }
29+
}
30+
31+
// MARK: - Private Properties
32+
33+
private let storage: FeatureStorage
34+
35+
// MARK: - Initializers
36+
37+
public init() {
38+
storage = FeatureStorage.shared
39+
}
40+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
//
2+
// Network.swift
3+
// CachedAsyncImage
4+
//
5+
// Created by Dmitry Kononchuk on 07.01.2024.
6+
// Copyright © 2024 Dmitry Kononchuk. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
@propertyWrapper
12+
struct Network {
13+
// MARK: - Public Properties
14+
15+
var wrappedValue: NetworkProtocol {
16+
get { storage.network }
17+
nonmutating set { storage.network = newValue }
18+
}
19+
20+
// MARK: - Private Properties
21+
22+
private let storage: FeatureStorage
23+
24+
// MARK: - Initializers
25+
26+
init() {
27+
storage = FeatureStorage.shared
28+
}
29+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// FeatureStorage.swift
3+
// CachedAsyncImage
4+
//
5+
// Created by Dmitry Kononchuk on 07.01.2024.
6+
// Copyright © 2024 Dmitry Kononchuk. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
final class FeatureStorage {
12+
// MARK: - Public Properties
13+
14+
var imageCache: ImageCacheProtocol = TemporaryImageCache()
15+
var network: NetworkProtocol = NetworkManager()
16+
17+
static let shared = FeatureStorage()
18+
19+
// MARK: - Private Initializers
20+
21+
private init() {}
22+
}

Sources/CachedAsyncImage/Services/ImageLoader.swift

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ final class ImageLoader: ObservableObject {
1818

1919
// MARK: - Private Properties
2020

21-
private let networkManager: NetworkManagerProtocol
22-
private let imageCache = TemporaryImageCache.shared
21+
private var imageCache: ImageCacheProtocol
22+
private let networkManager: NetworkProtocol
2323

2424
private var cancellables: Set<AnyCancellable> = []
2525
private(set) var isLoading = false
@@ -30,7 +30,8 @@ final class ImageLoader: ObservableObject {
3030

3131
// MARK: - Initializers
3232

33-
init(networkManager: NetworkManagerProtocol) {
33+
init(imageCache: ImageCacheProtocol, networkManager: NetworkProtocol) {
34+
self.imageCache = imageCache
3435
self.networkManager = networkManager
3536
}
3637

@@ -66,9 +67,7 @@ final class ImageLoader: ObservableObject {
6667
.map { CPImage(data: $0) }
6768
.catch { [weak self] error -> AnyPublisher<CPImage?, Never> in
6869
if let error = error as? NetworkError {
69-
DispatchQueue.main.async {
70-
self?.errorMessage = error.rawValue
71-
}
70+
self?.errorMessage(with: error.rawValue)
7271

7372
#if DEBUG
7473
print("**** CachedAsyncImage error: \(error.rawValue)")
@@ -103,18 +102,25 @@ final class ImageLoader: ObservableObject {
103102

104103
private func start() {
105104
isLoading = true
105+
errorMessage(with: nil)
106106
}
107107

108108
private func finish() {
109109
isLoading = false
110110
}
111111

112+
private func cancel() {
113+
cancellables.forEach { $0.cancel() }
114+
}
115+
112116
private func cache(url: URL?, image: CPImage?) {
113117
guard let url = url else { return }
114118
image.map { imageCache[url] = $0 }
115119
}
116120

117-
private func cancel() {
118-
cancellables.forEach { $0.cancel() }
121+
private func errorMessage(with text: String?) {
122+
Task { @MainActor [weak self] in
123+
self?.errorMessage = text
124+
}
119125
}
120126
}

Sources/CachedAsyncImage/Services/NetworkManager.swift

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,14 @@ enum NetworkError: LocalizedError {
3232
}
3333
}
3434

35-
protocol NetworkManagerProtocol {
35+
protocol NetworkProtocol {
3636
func fetchImage(from url: URL?) -> (
3737
progress: Progress?,
3838
publisher: AnyPublisher<Data, Error>
3939
)
4040
}
4141

42-
final class NetworkManager: NetworkManagerProtocol {
43-
// MARK: - Public Properties
44-
45-
static let shared = NetworkManager()
46-
47-
// MARK: - Private Initializers
48-
49-
private init() {}
50-
42+
struct NetworkManager: NetworkProtocol {
5143
// MARK: - Public Methods
5244

5345
func fetchImage(from url: URL?) -> (

Sources/CachedAsyncImage/Services/TemporaryImageCache.swift

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,30 @@
88

99
import Foundation
1010

11-
/// Temporary image cache.
12-
public final class TemporaryImageCache {
13-
// MARK: - Public Properties
11+
/// Image cache protocol.
12+
public protocol ImageCacheProtocol {
13+
subscript(_ url: URL) -> CPImage? { get set }
1414

15-
/// The singleton instance.
15+
/// Set cache limit.
1616
///
17-
/// - Returns: The singleton `TemporaryImageCache` instance.
18-
public static let shared = TemporaryImageCache()
17+
/// - Parameters:
18+
/// - countLimit: The maximum number of objects the cache should hold.
19+
/// If `0`, there is no count limit. The default value is `0`.
20+
/// - totalCostLimit: The maximum total cost that the cache can hold before
21+
/// it starts evicting objects.
22+
/// When you add an object to the cache, you may pass in a specified cost for the object,
23+
/// such as the size in bytes of the object.
24+
/// If `0`, there is no total cost limit. The default value is `0`.
25+
func setCacheLimit(countLimit: Int, totalCostLimit: Int)
1926

27+
/// Empties the cache.
28+
func removeCache()
29+
}
30+
31+
struct TemporaryImageCache: ImageCacheProtocol {
2032
// MARK: - Private Properties
2133

22-
private lazy var cache: NSCache<NSURL, CPImage> = {
34+
private let cache: NSCache<NSURL, CPImage> = {
2335
let cache = NSCache<NSURL, CPImage>()
2436
return cache
2537
}()
@@ -35,29 +47,14 @@ public final class TemporaryImageCache {
3547
}
3648
}
3749

38-
// MARK: - Private Initializers
39-
40-
private init() {}
41-
4250
// MARK: - Public Methods
4351

44-
/// Set cache limit.
45-
///
46-
/// - Parameters:
47-
/// - countLimit: The maximum number of objects the cache should hold.
48-
/// If `0`, there is no count limit. The default value is `0`.
49-
/// - totalCostLimit: The maximum total cost that the cache can hold before
50-
/// it starts evicting objects.
51-
/// When you add an object to the cache, you may pass in a specified cost for the object,
52-
/// such as the size in bytes of the object.
53-
/// If `0`, there is no total cost limit. The default value is `0`.
54-
public func setCacheLimit(countLimit: Int = 0, totalCostLimit: Int = 0) {
52+
func setCacheLimit(countLimit: Int = 0, totalCostLimit: Int = 0) {
5553
cache.countLimit = countLimit
5654
cache.totalCostLimit = totalCostLimit
5755
}
5856

59-
/// Empties the cache.
60-
public func removeCache() {
57+
func removeCache() {
6158
cache.removeAllObjects()
6259
}
6360
}

Sources/CachedAsyncImage/Views/CachedAsyncImage.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ public struct CachedAsyncImage: View {
5252
error: ((String) -> any View)? = nil
5353
) {
5454
_imageLoader = StateObject(
55-
wrappedValue: ImageLoader(networkManager: NetworkManager.shared)
55+
wrappedValue: ImageLoader(
56+
imageCache: ImageCache().wrappedValue,
57+
networkManager: Network().wrappedValue
58+
)
5659
)
5760

5861
self.url = url
@@ -75,7 +78,10 @@ public struct CachedAsyncImage: View {
7578
error: ((String) -> any View)? = nil
7679
) {
7780
_imageLoader = StateObject(
78-
wrappedValue: ImageLoader(networkManager: NetworkManager.shared)
81+
wrappedValue: ImageLoader(
82+
imageCache: ImageCache().wrappedValue,
83+
networkManager: Network().wrappedValue
84+
)
7985
)
8086

8187
self.url = url
@@ -98,7 +104,10 @@ public struct CachedAsyncImage: View {
98104
error: ((String) -> any View)? = nil
99105
) {
100106
_imageLoader = StateObject(
101-
wrappedValue: ImageLoader(networkManager: NetworkManager.shared)
107+
wrappedValue: ImageLoader(
108+
imageCache: ImageCache().wrappedValue,
109+
networkManager: Network().wrappedValue
110+
)
102111
)
103112

104113
self.url = url

0 commit comments

Comments
 (0)