Skip to content

Commit

Permalink
Merge pull request #6 from bullinnyc/add-image-cache-property-wrapper
Browse files Browse the repository at this point in the history
Add image cache property wrapper.
  • Loading branch information
bullinnyc authored Jan 8, 2024
2 parents b153880 + 93efc8e commit 32a1740
Show file tree
Hide file tree
Showing 11 changed files with 181 additions and 75 deletions.
12 changes: 6 additions & 6 deletions Examples/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct ContentView: View {
"https://image.tmdb.org/t/p/original/arw2vcBveWOVZr6pxd9XTd1TdQa.jpg"
]

private static let paddingStandart: CGFloat = 20
private static let standartPadding: CGFloat = 20

// MARK: - Body

Expand Down Expand Up @@ -77,7 +77,7 @@ struct ContentView: View {
}
)
.frame(
maxWidth: size.width - Self.paddingStandart * 2,
maxWidth: size.width - Self.standartPadding * 2,
idealHeight:
getIdealHeight(
geometrySize: size,
Expand All @@ -86,10 +86,10 @@ struct ContentView: View {
)
.clipped()
.clipShape(RoundedRectangle(cornerRadius: 20))
.padding([.leading, .trailing], Self.paddingStandart)
.padding([.leading, .trailing], Self.standartPadding)
}
}
.padding([.top, .bottom], Self.paddingStandart)
.padding([.top, .bottom], Self.standartPadding)
}
}
}
Expand All @@ -102,7 +102,7 @@ struct ContentView: View {

init() {
// Set image cache limit.
TemporaryImageCache.shared.setCacheLimit(
ImageCache().wrappedValue.setCacheLimit(
countLimit: 1000, // 1000 items
totalCostLimit: 1024 * 1024 * 200 // 200 MB
)
Expand All @@ -114,7 +114,7 @@ struct ContentView: View {
geometrySize: CGSize,
aspectRatio: CGFloat
) -> CGFloat {
let width = geometrySize.width - Self.paddingStandart * 2
let width = geometrySize.width - Self.standartPadding * 2
return width / aspectRatio
}
}
Expand Down
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,23 @@ CachedAsyncImage(
**Note:** The default value is `0`, e.g. is no count limit and is no total cost limit.

```swift
// Set image cache limit.
TemporaryImageCache.shared.setCacheLimit(
countLimit: 1000, // 1000 items
totalCostLimit: 1024 * 1024 * 200 // 200 MB
)
init() {
// Set image cache limit.
ImageCache().wrappedValue.setCacheLimit(
countLimit: 1000, // 1000 items
totalCostLimit: 1024 * 1024 * 200 // 200 MB
)
}
```

### You can also read this value from within a view to access the image cache management

```swift
struct MyView: View {
@ImageCache private var imageCache

// ...
}
```

## Requirements
Expand Down
40 changes: 40 additions & 0 deletions Sources/CachedAsyncImage/PropertyWrappers/ImageCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// ImageCache.swift
// CachedAsyncImage
//
// Created by Dmitry Kononchuk on 07.01.2024.
// Copyright © 2024 Dmitry Kononchuk. All rights reserved.
//

import Foundation

/// A property wrapper type that reflects a value from `TemporaryImageCache`.
///
/// Read this value from within a view to access the image cache management.
///
/// struct MyView: View {
/// @ImageCache private var imageCache
///
/// // ...
/// }
///
@propertyWrapper
public struct ImageCache {
// MARK: - Public Properties

/// The wrapped value property provides primary access to the value’s data.
public var wrappedValue: ImageCacheProtocol {
get { storage.imageCache }
nonmutating set { storage.imageCache = newValue }
}

// MARK: - Private Properties

private let storage: FeatureStorage

// MARK: - Initializers

public init() {
storage = FeatureStorage.shared
}
}
29 changes: 29 additions & 0 deletions Sources/CachedAsyncImage/PropertyWrappers/Network.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Network.swift
// CachedAsyncImage
//
// Created by Dmitry Kononchuk on 07.01.2024.
// Copyright © 2024 Dmitry Kononchuk. All rights reserved.
//

import Foundation

@propertyWrapper
struct Network {
// MARK: - Public Properties

var wrappedValue: NetworkProtocol {
get { storage.network }
nonmutating set { storage.network = newValue }
}

// MARK: - Private Properties

private let storage: FeatureStorage

// MARK: - Initializers

init() {
storage = FeatureStorage.shared
}
}
22 changes: 22 additions & 0 deletions Sources/CachedAsyncImage/Services/FeatureStorage.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// FeatureStorage.swift
// CachedAsyncImage
//
// Created by Dmitry Kononchuk on 07.01.2024.
// Copyright © 2024 Dmitry Kononchuk. All rights reserved.
//

import Foundation

final class FeatureStorage {
// MARK: - Public Properties

var imageCache: ImageCacheProtocol = TemporaryImageCache()
var network: NetworkProtocol = NetworkManager()

static let shared = FeatureStorage()

// MARK: - Private Initializers

private init() {}
}
22 changes: 14 additions & 8 deletions Sources/CachedAsyncImage/Services/ImageLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ final class ImageLoader: ObservableObject {

// MARK: - Private Properties

private let networkManager: NetworkManagerProtocol
private let imageCache = TemporaryImageCache.shared
private var imageCache: ImageCacheProtocol
private let networkManager: NetworkProtocol

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

// MARK: - Initializers

init(networkManager: NetworkManagerProtocol) {
init(imageCache: ImageCacheProtocol, networkManager: NetworkProtocol) {
self.imageCache = imageCache
self.networkManager = networkManager
}

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

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

private func start() {
isLoading = true
errorMessage(with: nil)
}

private func finish() {
isLoading = false
}

private func cancel() {
cancellables.forEach { $0.cancel() }
}

private func cache(url: URL?, image: CPImage?) {
guard let url = url else { return }
image.map { imageCache[url] = $0 }
}

private func cancel() {
cancellables.forEach { $0.cancel() }
private func errorMessage(with text: String?) {
Task { @MainActor [weak self] in
self?.errorMessage = text
}
}
}
12 changes: 2 additions & 10 deletions Sources/CachedAsyncImage/Services/NetworkManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,14 @@ enum NetworkError: LocalizedError {
}
}

protocol NetworkManagerProtocol {
protocol NetworkProtocol {
func fetchImage(from url: URL?) -> (
progress: Progress?,
publisher: AnyPublisher<Data, Error>
)
}

final class NetworkManager: NetworkManagerProtocol {
// MARK: - Public Properties

static let shared = NetworkManager()

// MARK: - Private Initializers

private init() {}

struct NetworkManager: NetworkProtocol {
// MARK: - Public Methods

func fetchImage(from url: URL?) -> (
Expand Down
45 changes: 21 additions & 24 deletions Sources/CachedAsyncImage/Services/TemporaryImageCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,30 @@

import Foundation

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

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

/// Empties the cache.
func removeCache()
}

struct TemporaryImageCache: ImageCacheProtocol {
// MARK: - Private Properties

private lazy var cache: NSCache<NSURL, CPImage> = {
private let cache: NSCache<NSURL, CPImage> = {
let cache = NSCache<NSURL, CPImage>()
return cache
}()
Expand All @@ -35,29 +47,14 @@ public final class TemporaryImageCache {
}
}

// MARK: - Private Initializers

private init() {}

// MARK: - Public Methods

/// Set cache limit.
///
/// - Parameters:
/// - countLimit: The maximum number of objects the cache should hold.
/// If `0`, there is no count limit. The default value is `0`.
/// - totalCostLimit: The maximum total cost that the cache can hold before
/// it starts evicting objects.
/// When you add an object to the cache, you may pass in a specified cost for the object,
/// such as the size in bytes of the object.
/// If `0`, there is no total cost limit. The default value is `0`.
public func setCacheLimit(countLimit: Int = 0, totalCostLimit: Int = 0) {
func setCacheLimit(countLimit: Int = 0, totalCostLimit: Int = 0) {
cache.countLimit = countLimit
cache.totalCostLimit = totalCostLimit
}

/// Empties the cache.
public func removeCache() {
func removeCache() {
cache.removeAllObjects()
}
}
15 changes: 12 additions & 3 deletions Sources/CachedAsyncImage/Views/CachedAsyncImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ public struct CachedAsyncImage: View {
error: ((String) -> any View)? = nil
) {
_imageLoader = StateObject(
wrappedValue: ImageLoader(networkManager: NetworkManager.shared)
wrappedValue: ImageLoader(
imageCache: ImageCache().wrappedValue,
networkManager: Network().wrappedValue
)
)

self.url = url
Expand All @@ -75,7 +78,10 @@ public struct CachedAsyncImage: View {
error: ((String) -> any View)? = nil
) {
_imageLoader = StateObject(
wrappedValue: ImageLoader(networkManager: NetworkManager.shared)
wrappedValue: ImageLoader(
imageCache: ImageCache().wrappedValue,
networkManager: Network().wrappedValue
)
)

self.url = url
Expand All @@ -98,7 +104,10 @@ public struct CachedAsyncImage: View {
error: ((String) -> any View)? = nil
) {
_imageLoader = StateObject(
wrappedValue: ImageLoader(networkManager: NetworkManager.shared)
wrappedValue: ImageLoader(
imageCache: ImageCache().wrappedValue,
networkManager: Network().wrappedValue
)
)

self.url = url
Expand Down
Loading

0 comments on commit 32a1740

Please sign in to comment.