Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add image cache property wrapper. #6

Merged
merged 3 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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