The Problem
While building iOS apps, I've always relied on image caching libraries like Kingfisher or SDWebImage. They're great, but I noticed a few pain points:
- Binary Size: Kingfisher adds ~500KB, SDWebImage ~800KB
- TTL Support: Limited time-to-live control for cached images
- Modern Swift: Not fully utilizing async/await patterns
- Complexity: Often using features I don't need
So I decided to build SwiftCacheSDK - a lightweight, zero-dependency image caching library that focuses on the essentials.
๐ฏ Design Goals
- Zero Dependencies: 100% Apple native APIs
- Lightweight: < 150KB binary size impact
- Modern Swift: Full async/await support
- Swift 6 Ready: Compatible with strict concurrency
- TTL Support: Automatic cache expiration
- Easy to Use: Drop-in replacement for existing solutions
๐๏ธ Architecture
Three-Tier Caching Strategy
SwiftCache implements a three-tier cache system:
Memory Cache (NSCache)
โ (miss)
Disk Cache (FileManager)
โ (miss)
Network (URLSession)
1. Memory Cache
- Uses
NSCachefor automatic eviction under memory pressure - Fastest access (~1ms)
- Survives within app session
private let memoryCache = NSCache<NSString, CacheEntry>()
private func setupMemoryCache() {
memoryCache.totalCostLimit = configuration.memoryCacheLimit
memoryCache.countLimit = configuration.memoryCacheCountLimit
memoryCache.name = "com.swiftcache.memory"
}
2. Disk Cache
- FileManager-based persistent storage
- LRU eviction when size limit reached
- SHA-256 hashed filenames to avoid collisions
private func diskCacheURL(for key: String) -> URL {
let hash = sha256Hash(of: key)
return diskCacheDirectory.appendingPathComponent("\(hash).jpg")
}
3. Network Layer
- Built on URLSession with URLCache
- Automatic retry and error handling
- Cancellable requests
๐ Key Features
1. Simple UIKit Integration
import SwiftCache
// Simple usage
imageView.sc.setImage(with: url)
// With placeholder and completion
imageView.sc.setImage(
with: url,
placeholder: UIImage(systemName: "photo")
) { result in
switch result {
case .success(let image):
print("Loaded: \(image.size)")
case .failure(let error):
print("Error: \(error)")
}
}
2. SwiftUI Native Support
import SwiftCache
struct ContentView: View {
var body: some View {
CachedImage(url: imageURL) {
ProgressView()
}
.frame(width: 300, height: 300)
}
}
3. Async/Await (iOS 15+)
Task {
do {
let image = try await imageView.sc.setImage(with: url)
print("Image loaded!")
} catch {
print("Failed: \(error)")
}
}
4. TTL Support
One unique feature is built-in TTL (time-to-live) support:
// Configure global TTL
SwiftCache.shared.configure { config in
config.defaultTTL = 3600 // 1 hour
}
// Or per-image TTL
imageView.sc.setImage(with: url, ttl: 600) // 10 minutes
5. Progressive Loading
Show thumbnails while loading full images:
imageView.sc.setImageProgressive(
with: fullImageURL,
thumbnailURL: thumbnailURL
)
6. Built-in Analytics
Track cache performance:
let metrics = SwiftCache.shared.getMetrics()
print("Hit rate: \(metrics.hitRate * 100)%")
print("Avg load time: \(metrics.averageLoadTime * 1000)ms")
print("Memory hits: \(metrics.memoryHits)")
print("Disk hits: \(metrics.diskHits)")
print("Network hits: \(metrics.networkHits)")
๐ง Technical Challenges
1. Swift 6 Concurrency
Getting the code to compile without warnings in Swift 6 strict concurrency mode was challenging. The main issues:
Problem: UIImageView properties are main-actor isolated
// โ This fails in Swift 6
imageView?.image = placeholder
Solution: Wrap in MainActor
// โ
Swift 6 compatible
await MainActor.run {
imageView?.image = placeholder
}
2. Async/Await Bridge
SwiftCache uses completion handlers internally but provides async/await APIs:
public func setImage(with url: URL) async throws -> UIImage {
try await withCheckedThrowingContinuation { continuation in
SwiftCache.shared.loadImage(from: url) { result in
continuation.resume(with: result)
}
}
}
3. Memory Management
Automatic memory cleanup on lifecycle events:
NotificationCenter.default.addObserver(
self,
selector: #selector(handleMemoryWarning),
name: UIApplication.didReceiveMemoryWarningNotification,
object: nil
)
@objc private func handleMemoryWarning() {
memoryCache.removeAllObjects()
}
4. Disk Cache Cleanup (LRU)
When disk cache exceeds limit, clean up oldest accessed files:
private func cleanupDiskCacheIfNeeded() {
let sortedFiles = files.sorted { file1, file2 in
let date1 = file1.contentAccessDate ?? .distantPast
let date2 = file2.contentAccessDate ?? .distantPast
return date1 < date2
}
// Remove oldest files until we're at 75% of limit
for fileURL in sortedFiles {
guard sizeToClean > targetSize else { break }
try? fileManager.removeItem(at: fileURL)
sizeToClean -= fileSize
}
}
๐ Performance Comparison
Benchmarked on iPhone 14 Pro, iOS 17:
| Metric | SwiftCache | Kingfisher |
|---|---|---|
| Binary Size | 150KB | 500KB |
| Memory Hit | ~1ms | ~5ms |
| Disk Hit | ~10ms | ~15ms |
| Network Load | ~200ms | ~200ms |
| Memory Usage | Lower | Higher |
๐ What I Learned
1. NSCache is Powerful
Apple's NSCache handles memory management beautifully - no need to reinvent the wheel.
2. Swift 6 Concurrency is Worth It
The strict checking caught several potential race conditions early.
3. Zero Dependencies is Achievable
You don't always need external frameworks. Foundation and UIKit provide powerful APIs.
4. API Design Matters
Making the API intuitive (imageView.sc.setImage) encourages adoption.
5. Documentation is Key
Good docs (README, examples, migration guides) make or break open source projects.
๐ฎ Future Plans (v1.1.0)
- Combine support for reactive streams
- GIF animation support
- WebP format support
- Custom image processors
- Network reachability awareness
๐ Try It Out
SwiftCache is available now:
- GitHub: github.com/SudhirGadhvi/SwiftCache-SDK
- Installation: Swift Package Manager
- License: MIT
dependencies: [
.package(url: "https://github.com/SudhirGadhvi/SwiftCache-SDK", from: "1.0.0")
]
๐ญ Final Thoughts
Building SwiftCache taught me that:
- Sometimes "less is more"
- Modern Swift is incredibly powerful
- The iOS community appreciates lightweight solutions
- Open source is about solving real problems
If you're building iOS apps that load images, give SwiftCache a try. And if you have feedback or want to contribute, I'd love to hear from you!
โญ If you found this useful, give it a star on GitHub!
๐ฌ Questions or feedback? Drop a comment below or open a discussion on GitHub.
๐ฆ Follow me on Twitter: @SudhirGadhvi
Top comments (0)