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 protocol. #5

Merged
merged 3 commits into from
Jan 2, 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
2 changes: 1 addition & 1 deletion Examples/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ struct ContentView: View {

init() {
// Set image cache limit.
TemporaryImageCache.shared.setCacheLimit(
Environment(\.imageCache).wrappedValue.setCacheLimit(
countLimit: 1000, // 1000 items
totalCostLimit: 1024 * 1024 * 200 // 200 MB
)
Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,13 @@ 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.
Environment(\.imageCache).wrappedValue.setCacheLimit(
countLimit: 1000, // 1000 items
totalCostLimit: 1024 * 1024 * 200 // 200 MB
)
}
```

## Requirements
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
//
// EnvironmentValues + ImageCache.swift
// CachedAsyncImage
//
// Created by Dmitry Kononchuk on 02.01.2024.
// Copyright © 2024 Dmitry Kononchuk. All rights reserved.
//

import SwiftUI

struct ImageCacheKey: EnvironmentKey {
// MARK: - Public Properties

static let defaultValue: ImageCache = TempImageCache()
}

extension EnvironmentValues {
// MARK: - Public Properties

/// The image cache of this environment.
///
/// Read this environment value from within a view to access the image cache management.
///
/// struct MyView: View {
/// @Environment(\.imageCache) private var imageCache
///
/// // ...
/// }
public var imageCache: ImageCache {
get { self[ImageCacheKey.self] }
set { self[ImageCacheKey.self] = newValue }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// EnvironmentValues + NetworkManager.swift
// CachedAsyncImage
//
// Created by Dmitry Kononchuk on 02.01.2024.
// Copyright © 2024 Dmitry Kononchuk. All rights reserved.
//

import SwiftUI

struct NetworkManagerKey: EnvironmentKey {
// MARK: - Public Properties

static let defaultValue: NetworkManagerProtocol = NetworkManager()
}

extension EnvironmentValues {
// MARK: - Public Properties

var networkManager: NetworkManagerProtocol {
get { self[NetworkManagerKey.self] }
set { self[NetworkManagerKey.self] = newValue }
}
}
5 changes: 3 additions & 2 deletions Sources/CachedAsyncImage/Services/ImageLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ final class ImageLoader: ObservableObject {
// MARK: - Private Properties

private let networkManager: NetworkManagerProtocol
private let imageCache = TemporaryImageCache.shared
private var imageCache: ImageCache

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

// MARK: - Initializers

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

// MARK: - Deinitializers
Expand Down
10 changes: 1 addition & 9 deletions Sources/CachedAsyncImage/Services/NetworkManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,7 @@ protocol NetworkManagerProtocol {
)
}

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

static let shared = NetworkManager()

// MARK: - Private Initializers

private init() {}

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

func fetchImage(from url: URL?) -> (
Expand Down
61 changes: 61 additions & 0 deletions Sources/CachedAsyncImage/Services/TempImageCache.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
//
// TempImageCache.swift
// CachedAsyncImage
//
// Created by Dmitry Kononchuk on 02.01.2024.
// Copyright © 2024 Dmitry Kononchuk. All rights reserved.
//

import Foundation

/// Image cache protocol.
public protocol ImageCache {
subscript(_ url: URL) -> CPImage? { get set }

/// 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`.
func setCacheLimit(countLimit: Int, totalCostLimit: Int)

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

/// Temporary image cache.
struct TempImageCache: ImageCache {
// MARK: - Private Properties

private let cache: NSCache<NSURL, CPImage> = {
let cache = NSCache<NSURL, CPImage>()
return cache
}()

// MARK: - Subscripts

public subscript(_ key: URL) -> CPImage? {
get { cache.object(forKey: key as NSURL) }
set {
newValue == nil
? cache.removeObject(forKey: key as NSURL)
: cache.setObject(newValue ?? CPImage(), forKey: key as NSURL)
}
}

// MARK: - Public Methods

public func setCacheLimit(countLimit: Int = 0, totalCostLimit: Int = 0) {
cache.countLimit = countLimit
cache.totalCostLimit = totalCostLimit
}

public func removeCache() {
cache.removeAllObjects()
}
}
29 changes: 14 additions & 15 deletions Sources/CachedAsyncImage/Services/TemporaryImageCache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@

import Foundation

/// Will be removed in future versions.
var isTemporaryImageCacheInitialized = false

/// Temporary image cache.
public final class TemporaryImageCache {
@available(
*,
deprecated,
message: "Will be removed in future versions. Use Environment with key path 'imageCache' property."
)
public final class TemporaryImageCache: ImageCache {
// MARK: - Public Properties

/// The singleton instance.
Expand All @@ -19,14 +27,14 @@ public final class TemporaryImageCache {

// MARK: - Private Properties

private lazy var cache: NSCache<NSURL, CPImage> = {
private let cache: NSCache<NSURL, CPImage> = {
let cache = NSCache<NSURL, CPImage>()
return cache
}()

// MARK: - Subscripts

subscript(_ key: URL) -> CPImage? {
public subscript(_ key: URL) -> CPImage? {
get { cache.object(forKey: key as NSURL) }
set {
newValue == nil
Expand All @@ -37,26 +45,17 @@ public final class TemporaryImageCache {

// MARK: - Private Initializers

private init() {}
private init() {
isTemporaryImageCacheInitialized = true
}

// 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) {
cache.countLimit = countLimit
cache.totalCostLimit = totalCostLimit
}

/// Empties the cache.
public func removeCache() {
cache.removeAllObjects()
}
Expand Down
26 changes: 23 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(
networkManager: Environment(\.networkManager).wrappedValue,
imageCache: CachedAsyncImage.getImageCache()
)
)

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(
networkManager: Environment(\.networkManager).wrappedValue,
imageCache: CachedAsyncImage.getImageCache()
)
)

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(
networkManager: Environment(\.networkManager).wrappedValue,
imageCache: CachedAsyncImage.getImageCache()
)
)

self.url = url
Expand All @@ -108,6 +117,17 @@ public struct CachedAsyncImage: View {

placeholderWithProgress = placeholder
}

// MARK: - Private Methods

/// Will be removed in future versions.
private static func getImageCache() -> ImageCache {
if isTemporaryImageCacheInitialized {
TemporaryImageCache.shared
} else {
Environment(\.imageCache).wrappedValue
}
}
}

// MARK: - Ext. Configure views
Expand Down
21 changes: 14 additions & 7 deletions Tests/CachedAsyncImageTests/ImageLoaderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,14 @@ final class ImageLoaderTests: XCTestCase {
func testFetchImage_WithCachedImage() {
// Given
let url = "https://example.com/image.jpg"
let networkManager = sut.networkManager
let imageCache = sut.imageCache
let cachedImage = RM.image("backToTheFuture")
let networkManager = sut.networkManager
var imageCache = sut.imageCache

let imageLoader = ImageLoader(networkManager: networkManager)
let imageLoader = ImageLoader(
networkManager: networkManager,
imageCache: imageCache
)

// When
guard let imageUrl = URL(string: url) else {
Expand All @@ -58,10 +61,14 @@ final class ImageLoaderTests: XCTestCase {
// Given
let url = "https://example.com/image.jpg"
let networkManager = sut.networkManager

let imageCache = sut.imageCache
imageCache.removeCache()

let imageLoader = ImageLoader(networkManager: networkManager)
let imageLoader = ImageLoader(
networkManager: networkManager,
imageCache: imageCache
)

// When
imageLoader.fetchImage(from: url)
Expand Down Expand Up @@ -117,12 +124,12 @@ final class ImageLoaderTests: XCTestCase {
extension ImageLoaderTests {
typealias Sut = (
networkManager: NetworkManagerProtocol,
imageCache: TemporaryImageCache
imageCache: ImageCache
)

private func makeSUT() -> Sut {
let networkManager = NetworkManagerMock.shared
let imageCache = TemporaryImageCache.shared
let networkManager = NetworkManagerMock()
let imageCache = TempImageCache()

return (networkManager, imageCache)
}
Expand Down
10 changes: 1 addition & 9 deletions Tests/CachedAsyncImageTests/Mocks/NetworkManagerMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,7 @@ import Foundation
import Combine
@testable import CachedAsyncImage

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

static let shared = NetworkManagerMock()

// MARK: - Private Initializers

private init() {}

struct NetworkManagerMock: NetworkManagerProtocol {
// MARK: - Public Methods

func fetchImage(from url: URL?) -> (
Expand Down