DEV Community

Sudhir Gadhvi
Sudhir Gadhvi

Posted on

Building SwiftCacheSDK: A Lightweight Image Caching Library for iOS

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

  1. Zero Dependencies: 100% Apple native APIs
  2. Lightweight: < 150KB binary size impact
  3. Modern Swift: Full async/await support
  4. Swift 6 Ready: Compatible with strict concurrency
  5. TTL Support: Automatic cache expiration
  6. 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 NSCache for 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:

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)