
“Zeus gave man Pandora, a beautiful evil … and from her jar flowed every misfortune that haunts humanity, leaving only hope left inside.”
— Aeschylus
A powerful, type-safe caching library for Swift that provides multiple storage strategies with a unified API. Built with Swift Concurrency, Combine integration, and modern Swift best practices.
- Features
- Installation
- Quick Start
- Cache Types
- Type Declaration Options
- Advanced Usage
- Thread Safety
- Clean Architecture Example Usage
- License
✨ Multiple Storage Strategies
- Memory Cache: Fast in-memory storage with LRU eviction and optional TTL
- Disk Cache: Persistent file-based storage with actor isolation and optional TTL
- Hybrid Cache: Combines memory + disk with concurrent load deduplication
- UserDefaults Cache: Namespaced, type-safe storage with optional iCloud sync, global limits, and per-item size caps
- Lightweight: ~1.5MB, zero dependencies
🚀 Modern Swift Architecture
- Built on Swift Concurrency (
async/await
) - Actor isolation for safe persistence without manual locks
- Generic, type-safe APIs
- Combine publishers for reactive data flow
⚡ Performance
- LRU eviction in memory & disk
- Per-entry and global TTLs
- Concurrent load deduplication in HybridBox (
inflight
task pooling) - Namespace-based cache separation
Add Pandora to your project using Xcode or by adding it to your Package.swift
:
dependencies: [
.package(url: "https://github.com/joshgallantt/Pandora.git", from: "3.2.0")
]
import Pandora
// Memory cache — fast, in-memory only
let memoryBox: PandoraMemoryBox<String, User> = Pandora.Memory.box()
memoryBox.put(key: "user123", value: user)
let cachedUser = memoryBox.get("user123")
// Disk cache — persistent, actor-isolated
let diskBox: PandoraDiskBox<String, User> = Pandora.Disk.box(namespace: "users")
await diskBox.put(key: "user123", value: user)
let persistedUser = await diskBox.get("user123")
// Hybrid cache — memory first, disk fallback, async hydration
let hybridBox: PandoraHybridBox<String, User> = Pandora.Hybrid.box(namespace: "users")
hybridBox.put(key: "user123", value: user)
let hybridUser = await hybridBox.get("user123")
// UserDefaults cache — type-safe key-value store with optional iCloud sync
let defaultsBox: PandoraUserDefaultsBox<User> = Pandora.UserDefaults.box(
namespace: "user_defaults",
iCloudBacked: true // default: true
)
defaultsBox.put(key: "user123", value: user)
let defaultsUser = await defaultsBox.get("user123")
Perfect for frequently accessed data that doesn't need persistence.
let box: PandoraMemoryBox<String, Data> = Pandora.Memory.box(
maxSize: 1000,
expiresAfter: 3600
)
box.put(key: "thumb", value: imageData)
let data = box.get("thumb")
box.publisher(for: "thumb")
.sink { /* react to updates */ }
.store(in: &cancellables)
Actor-isolated persistent storage for data that survives app restarts.
let box: PandoraDiskBox<String, UserProfile> = Pandora.Disk.box(
namespace: "profiles",
maxSize: 10000,
expiresAfter: 86400
)
await box.put(key: "p1", value: userProfile)
let profile = await box.get("p1")
Combines memory and disk storage for optimal performance and persistence.
let box: PandoraHybridBox<String, APIResponse> = Pandora.Hybrid.box(
namespace: "api_cache",
memoryMaxSize: 500,
memoryExpiresAfter: 300,
diskMaxSize: 5000,
diskExpiresAfter: 3600
)
box.put(key: "resp", value: response)
let cached = await box.get("resp")
box.publisher(for: "resp")
.sink { updateUI($0) }
.store(in: &cancellables)
Type-safe UserDefaults
storage with namespace isolation,
optional iCloud synchronization.
let settingsBox: PandoraUserDefaultsBox<String> =
Pandora.UserDefaults.box(namespace: "settings")
settingsBox.put(key: "username", value: "john")
let username = await settingsBox.get("username")
Warning
- Max 1024 items across all
UserDefaultsBox
instances - Max 1KB per stored value
- Enforced globally`
Tip
To enable iCloud synchronization, you must add the iCloud capability in your Xcode target’s Signing & Capabilities tab, and under iCloud services check Key-Value storage. Without this, iCloud-backed UserDefaults
(via NSUbiquitousKeyValueStore
) will not work.
Pandora boxes are generic over their key and value types (except UserDefaults
, which is generic only over the value type).
There are three ways to specify those types depending on context.
// Memory, Disk, and Hybrid require both Key and Value types
let memoryBox: PandoraMemoryBox<String, User> = Pandora.Memory.box()
let diskBox: PandoraDiskBox<String, User> = Pandora.Disk.box(namespace: "users")
let hybridBox: PandoraHybridBox<String, User> = Pandora.Hybrid.box(namespace: "users")
// UserDefaults requires only Value type
let defaultsBox: PandoraUserDefaultsBox<User> = Pandora.UserDefaults.box(namespace: "users")
let memoryBox = Pandora.Memory.box() as PandoraMemoryBox<String, User>
let diskBox = Pandora.Disk.box(namespace: "users") as PandoraDiskBox<String, User>
let hybridBox = Pandora.Hybrid.box(namespace: "users") as PandoraHybridBox<String, User>
let defaultsBox = Pandora.UserDefaults.box(namespace: "users") as PandoraUserDefaultsBox<User>
Useful when Swift can’t infer types or when constructing dynamically (e.g., in generic or factory contexts).
// Memory, Disk, Hybrid
let memoryBox = Pandora.Memory.box(
keyType: String.self,
valueType: User.self
)
let diskBox = Pandora.Disk.box(
namespace: "users",
keyType: String.self,
valueType: User.self
)
let hybridBox = Pandora.Hybrid.box(
namespace: "users",
keyType: String.self,
valueType: User.self
)
// UserDefaults only requires Value type
let defaultsBox = Pandora.UserDefaults.box(
namespace: "users",
valueType: User.self
)
Tip
Explicit type parameters are especially useful inside generic or factory contexts where the return type isn’t obvious.
let cache: PandoraMemoryBox<String, Data> = Pandora.Memory.box()
// Store with custom TTL
cache.put(
key: "short_lived_data",
value: data,
expiresAfter: 60 // 1 minute
)
// Store without expiration (overrides global TTL)
cache.put(
key: "permanent_data",
value: data,
expiresAfter: nil
)
let cache: PandoraMemoryBox<String, User> = Pandora.Memory.box()
// Observe specific keys
cache.publisher(for: "current_user")
.compactMap { $0 } // Filter out nil values
.sink { user in
print("User updated: \(user.name)")
}
.store(in: &cancellables)
// Chain multiple cache operations
cache.publisher(for: "user_id")
.compactMap { $0 }
.flatMap { userId in
fetchUserDetails(userId)
}
.sink { userDetails in
// Handle user details
}
.store(in: &cancellables)
// Clear specific cache
cache.clear()
await diskCache.clear()
// Remove all Pandora disk caches for this app
Pandora.clearAllDiskData()
// Remove all keys from this app's UserDefaults and iCloud KVS
Pandora.clearUserDefaults()
// Remove everything above (nuclear option)
Pandora.deleteAllLocalStorage()
All Pandora cache types are designed for concurrent access:
- MemoryBox: Lock-based thread safety
- DiskBox: Actor-isolated
- HybridBox: Locks for memory + inflight tracking, actor-isolated disk
- UserDefaultsBox: Locks + optional iCloud sync
1. Create your repository and initialise the Cache
import Pandora
import Combine
final class WishlistRepository {
private let cache: PandoraMemoryBox<String, Set<String>>
private let service: WishlistService
private let wishlistKey = "wishlist"
init(service: WishlistService) {
self.service = service
self.cache = Pandora.Memory.box(
maxSize: 1000,
expiresAfter: 3600 // 1 hour TTL
)
}
func observeIsWishlisted(productID: String) -> AnyPublisher<Bool, Never> {
cache.publisher(for: wishlistKey)
.map { ids in ids?.contains(productID) ?? false }
.eraseToAnyPublisher()
}
func addToWishlist(productID: String) async throws {
let updatedIDs = try await service.addProduct(productID: productID)
cache.put(key: wishlistKey, value: Set(updatedIDs))
}
func removeFromWishlist(productID: String) async throws {
let updatedIDs = try await service.removeProduct(productID: productID)
cache.put(key: wishlistKey, value: Set(updatedIDs))
}
}
2. Use Cases use the Cache
struct ObserveProductInWishlistUseCase {
private let repository: WishlistRepository
init(repository: WishlistRepository) { self.repository = repository }
func execute(productID: String) -> AnyPublisher<Bool, Never> {
repository.observeIsWishlisted(productID: productID)
.removeDuplicates() // Ensures only changes are delivered to ViewModel
.eraseToAnyPublisher()
}
}
struct AddProductToWishlistUseCase {
private let repository: WishlistRepository
init(repository: WishlistRepository) { self.repository = repository }
func execute(productID: String) async throws {
try await repository.addToWishlist(productID: productID)
}
}
struct RemoveProductFromWishlistUseCase {
private let repository: WishlistRepository
init(repository: WishlistRepository) { self.repository = repository }
func execute(productID: String) async throws {
try await repository.removeFromWishlist(productID: productID)
}
}
3. ViewModels use the Use Cases
import Combine
import Foundation
@MainActor
final class WishlistButtonViewModel: ObservableObject {
@Published private(set) var isWishlisted: Bool = false
private let productID: String
private let observeProductInWishlist: ObserveProductInWishlistUseCase
private let addProductToWishlist: AddProductToWishlistUseCase
private let removeProductFromWishlist: RemoveProductFromWishlistUseCase
private var cancellables = Set<AnyCancellable>()
init(
productID: String,
observeProductInWishlist: ObserveProductInWishlistUseCase,
addProductToWishlist: AddProductToWishlistUseCase,
removeProductFromWishlist: RemoveProductFromWishlistUseCase
) {
self.productID = productID
self.observeProductInWishlist = observeProductInWishlist
self.addProductToWishlist = addProductToWishlist
self.removeProductFromWishlist = removeProductFromWishlist
observeWishlistState()
}
private func observeWishlistState() {
observeProductInWishlist.execute(productID: productID)
.receive(on: DispatchQueue.main)
.assign(to: &$isWishlisted)
}
func toggleWishlist() {
let newValue = !isWishlisted
isWishlisted = newValue
Task(priority: .userInitiated) { [self, newValue] in
do {
if newValue {
try await addProductToWishlist.execute(productID: productID)
} else {
try await removeProductFromWishlist.execute(productID: productID)
}
} catch {
await MainActor.run {
isWishlisted = !newValue
}
}
}
}
}
This project is licensed under the MIT License - see the LICENSE file for details.
Created with ❤️ by Josh Gallant - for the Swift community.