Skip to content

Commit

Permalink
Merge pull request #5 from bullinnyc/add-image-cache-protocol
Browse files Browse the repository at this point in the history
Add image cache protocol.
  • Loading branch information
bullinnyc authored Jan 2, 2024
2 parents b153880 + e39d82a commit e0885d4
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 51 deletions.
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

0 comments on commit e0885d4

Please sign in to comment.