|
| 1 | +# JMImageCache |
| 2 | + |
| 3 | +A fast, simple, and lightweight image caching library for iOS, macOS, tvOS, and watchOS. |
| 4 | + |
| 5 | +`JMImageCache` provides both in-memory (NSCache) and disk-based caching for images downloaded from the network. It supports modern Swift async/await patterns while maintaining full backward compatibility with the original Objective-C API. |
| 6 | + |
| 7 | +## Features |
| 8 | + |
| 9 | +- **In-memory caching** - Automatic memory management via NSCache |
| 10 | +- **Disk caching** - Persistent storage with SHA1-hashed filenames |
| 11 | +- **Async/await support** - Modern Swift concurrency patterns |
| 12 | +- **SwiftUI support** - `CachedAsyncImage` view component |
| 13 | +- **UIKit support** - `UIImageView` extension for easy image loading |
| 14 | +- **Backward compatible** - Original Objective-C API still works |
| 15 | +- **Cross-platform** - iOS, macOS, tvOS, and watchOS |
| 16 | +- **Thread-safe** - All operations are properly synchronized |
| 17 | +- **Lightweight** - No external dependencies |
| 18 | + |
| 19 | +## Requirements |
| 20 | + |
| 21 | +- iOS 15.0+ / macOS 12.0+ / tvOS 15.0+ / watchOS 8.0+ |
| 22 | +- Swift 5.9+ |
| 23 | +- Xcode 15.0+ |
| 24 | + |
| 25 | +For older iOS versions (5.0-14.x), use the legacy Objective-C implementation included in this repo. |
| 26 | + |
| 27 | +## Installation |
| 28 | + |
| 29 | +### Swift Package Manager (Recommended) |
| 30 | + |
| 31 | +Add JMImageCache to your project via SPM: |
| 32 | + |
| 33 | +```swift |
| 34 | +dependencies: [ |
| 35 | + .package(url: "https://github.com/jakemarsh/JMImageCache.git", from: "2.0.0") |
| 36 | +] |
| 37 | +``` |
| 38 | + |
| 39 | +Or in Xcode: File → Add Package Dependencies → Enter the repository URL. |
| 40 | + |
| 41 | +### CocoaPods (Legacy) |
| 42 | + |
| 43 | +```ruby |
| 44 | +pod 'JMImageCache' |
| 45 | +``` |
| 46 | + |
| 47 | +## Quick Start |
| 48 | + |
| 49 | +### Swift (Async/Await) |
| 50 | + |
| 51 | +```swift |
| 52 | +import JMImageCache |
| 53 | + |
| 54 | +// Simple async/await |
| 55 | +let image = try await JMImageCache.shared.image(for: url) |
| 56 | + |
| 57 | +// With custom cache key |
| 58 | +let image = try await JMImageCache.shared.image(for: url, key: "my-custom-key") |
| 59 | +``` |
| 60 | + |
| 61 | +### Swift (Completion Handler) |
| 62 | + |
| 63 | +```swift |
| 64 | +JMImageCache.shared.image(for: url) { image in |
| 65 | + imageView.image = image |
| 66 | +} failure: { error in |
| 67 | + print("Failed: \(error)") |
| 68 | +} |
| 69 | +``` |
| 70 | + |
| 71 | +### SwiftUI |
| 72 | + |
| 73 | +```swift |
| 74 | +import JMImageCache |
| 75 | + |
| 76 | +struct ContentView: View { |
| 77 | + var body: some View { |
| 78 | + CachedAsyncImage(url: URL(string: "https://example.com/image.jpg")) { phase in |
| 79 | + switch phase { |
| 80 | + case .empty: |
| 81 | + ProgressView() |
| 82 | + case .success(let image): |
| 83 | + image |
| 84 | + .resizable() |
| 85 | + .aspectRatio(contentMode: .fit) |
| 86 | + case .failure: |
| 87 | + Image(systemName: "photo") |
| 88 | + .foregroundColor(.gray) |
| 89 | + } |
| 90 | + } |
| 91 | + } |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +### UIKit |
| 96 | + |
| 97 | +```swift |
| 98 | +import JMImageCache |
| 99 | + |
| 100 | +// Simple usage |
| 101 | +imageView.setImage(with: url) |
| 102 | + |
| 103 | +// With placeholder |
| 104 | +imageView.setImage(with: url, placeholder: UIImage(named: "placeholder")) |
| 105 | + |
| 106 | +// With callbacks |
| 107 | +imageView.setImage(with: url, placeholder: placeholderImage) { image in |
| 108 | + print("Loaded!") |
| 109 | +} failure: { error in |
| 110 | + print("Error: \(error)") |
| 111 | +} |
| 112 | + |
| 113 | +// Cancel loading |
| 114 | +imageView.cancelImageLoad() |
| 115 | +``` |
| 116 | + |
| 117 | +### Objective-C (Legacy) |
| 118 | + |
| 119 | +```objc |
| 120 | +#import "JMImageCache.h" |
| 121 | + |
| 122 | +// UIImageView category |
| 123 | +[imageView setImageWithURL:url placeholder:placeholderImage]; |
| 124 | + |
| 125 | +// Direct cache access |
| 126 | +[[JMImageCache sharedCache] imageForURL:url completionBlock:^(UIImage *image) { |
| 127 | + self.imageView.image = image; |
| 128 | +}]; |
| 129 | +``` |
| 130 | +
|
| 131 | +## How It Works |
| 132 | +
|
| 133 | +Images can be in three states: |
| 134 | +
|
| 135 | +1. **Cached In Memory** - Returned immediately |
| 136 | +2. **Cached On Disk** - Loaded from disk, moved to memory, returned |
| 137 | +3. **Not Cached** - Downloaded, saved to disk, cached in memory, returned |
| 138 | +
|
| 139 | +This approach ensures the fastest possible image delivery while minimizing network requests. |
| 140 | +
|
| 141 | +## Cache Management |
| 142 | +
|
| 143 | +### Clear All |
| 144 | +
|
| 145 | +```swift |
| 146 | +// Swift |
| 147 | +JMImageCache.shared.removeAllImages() |
| 148 | +
|
| 149 | +// Objective-C |
| 150 | +[[JMImageCache sharedCache] removeAllObjects]; |
| 151 | +``` |
| 152 | + |
| 153 | +### Remove Specific Image |
| 154 | + |
| 155 | +```swift |
| 156 | +// Swift |
| 157 | +JMImageCache.shared.removeImage(for: url) |
| 158 | +JMImageCache.shared.removeImage(forKey: "my-key") |
| 159 | + |
| 160 | +// Objective-C |
| 161 | +[[JMImageCache sharedCache] removeImageForURL:url]; |
| 162 | +``` |
| 163 | + |
| 164 | +### Pre-cache Images |
| 165 | + |
| 166 | +```swift |
| 167 | +// Swift |
| 168 | +let image = try await JMImageCache.shared.image(for: url) |
| 169 | +// Image is now cached for future use |
| 170 | +``` |
| 171 | + |
| 172 | +### Check Cache |
| 173 | + |
| 174 | +```swift |
| 175 | +// Swift |
| 176 | +if let cached = JMImageCache.shared.cachedImage(for: url) { |
| 177 | + // Image is in memory cache |
| 178 | +} |
| 179 | +``` |
| 180 | + |
| 181 | +## Custom Configuration |
| 182 | + |
| 183 | +```swift |
| 184 | +// Custom cache directory |
| 185 | +let cache = JMImageCache( |
| 186 | + cacheDirectory: customURL, |
| 187 | + urlSession: .shared |
| 188 | +) |
| 189 | +``` |
| 190 | + |
| 191 | +## Migration from v1.x (Objective-C) |
| 192 | + |
| 193 | +The Swift rewrite maintains API compatibility. Most Objective-C code will continue to work. For Swift projects: |
| 194 | + |
| 195 | +| Old API (Obj-C) | New API (Swift) | |
| 196 | +|-----------------|-----------------| |
| 197 | +| `[imageView setImageWithURL:url placeholder:placeholder]` | `imageView.setImage(with: url, placeholder: placeholder)` | |
| 198 | +| `[[JMImageCache sharedCache] imageForURL:url completionBlock:^...]` | `try await JMImageCache.shared.image(for: url)` | |
| 199 | +| `[[JMImageCache sharedCache] cachedImageForURL:url]` | `JMImageCache.shared.cachedImage(for: url)` | |
| 200 | +| `[[JMImageCache sharedCache] removeAllObjects]` | `JMImageCache.shared.removeAllImages()` | |
| 201 | + |
| 202 | +## Error Handling |
| 203 | + |
| 204 | +```swift |
| 205 | +do { |
| 206 | + let image = try await JMImageCache.shared.image(for: url) |
| 207 | +} catch JMImageCacheError.invalidResponse(let response) { |
| 208 | + // Server returned an error |
| 209 | +} catch JMImageCacheError.invalidImageData { |
| 210 | + // Data couldn't be converted to an image |
| 211 | +} catch { |
| 212 | + // Other error |
| 213 | +} |
| 214 | +``` |
| 215 | + |
| 216 | +## Thread Safety |
| 217 | + |
| 218 | +All JMImageCache operations are thread-safe: |
| 219 | + |
| 220 | +- Memory cache operations are synchronized via NSCache |
| 221 | +- Disk operations run on a dedicated serial queue |
| 222 | +- Completion handlers and async results are always delivered on the main thread |
| 223 | + |
| 224 | +## Demo App |
| 225 | + |
| 226 | +The repository includes a demo project showing typical usage in a `UITableViewController` with image loading. |
| 227 | + |
| 228 | +## License |
| 229 | + |
| 230 | +JMImageCache is available under the MIT license. See the LICENSE file for details. |
| 231 | + |
| 232 | +## Author |
| 233 | + |
| 234 | +Jake Marsh ([@jakemarsh](https://twitter.com/jakemarsh)) |
| 235 | + |
| 236 | +Originally created in 2011, rewritten in Swift with async/await in 2024. |
0 commit comments