Convert Figma logo to code with AI

hyperoslo logoCache

:package: Nothing but Cache.

2,963
335
2,963
5

Top Related Projects

1,153

RxSwift extension for RealmSwift's types

Realm is a mobile database: a replacement for Core Data & SQLite

A type-safe, Swift-language layer over SQLite3.

A toolkit for SQLite databases, with a focus on application development

Simple Swift wrapper for Keychain that works on iOS, watchOS, tvOS and macOS.

The better way to deal with JSON data in Swift.

Quick Overview

Cache is a Swift library that provides a versatile caching solution for iOS, macOS, tvOS, and watchOS applications. It offers a simple interface for storing and retrieving various types of data, including images, JSON, and custom objects, with support for both memory and disk storage.

Pros

  • Easy to use API with a clean and intuitive interface
  • Supports multiple storage types (memory and disk)
  • Highly customizable with options for expiration dates, size limits, and more
  • Thread-safe implementation for concurrent access

Cons

  • Limited built-in support for complex data structures
  • May require additional configuration for optimal performance in large-scale applications
  • Documentation could be more comprehensive for advanced use cases

Code Examples

  1. Basic usage for storing and retrieving a string:
let storage = try? Storage<String, String>(
    diskConfig: DiskConfig(name: "Strings"),
    memoryConfig: MemoryConfig(),
    transformer: TransformerFactory.forCodable(ofType: String.self)
)

try? storage?.setObject("Hello, Cache!", forKey: "greeting")
let greeting = try? storage?.object(forKey: "greeting")
print(greeting ?? "No greeting found")
  1. Storing and retrieving an image:
let storage = try? Storage<String, Image>(
    diskConfig: DiskConfig(name: "Images"),
    memoryConfig: MemoryConfig(),
    transformer: TransformerFactory.forImage()
)

let image = UIImage(named: "example")
try? storage?.setObject(image, forKey: "exampleImage")
let retrievedImage = try? storage?.object(forKey: "exampleImage")
  1. Using expiration dates:
let storage = try? Storage<String, String>(
    diskConfig: DiskConfig(name: "ExpiringStrings"),
    memoryConfig: MemoryConfig(),
    transformer: TransformerFactory.forCodable(ofType: String.self)
)

let expirationDate = Date().addingTimeInterval(3600) // 1 hour from now
try? storage?.setObject("Temporary data", forKey: "tempKey", expiry: .date(expirationDate))

Getting Started

  1. Add Cache to your project using Swift Package Manager:

    dependencies: [
        .package(url: "https://github.com/hyperoslo/Cache.git", from: "6.0.0")
    ]
    
  2. Import Cache in your Swift file:

    import Cache
    
  3. Create a storage instance:

    let storage = try? Storage<String, String>(
        diskConfig: DiskConfig(name: "MyStorage"),
        memoryConfig: MemoryConfig(),
        transformer: TransformerFactory.forCodable(ofType: String.self)
    )
    
  4. Start using Cache to store and retrieve data:

    try? storage?.setObject("Hello, World!", forKey: "greeting")
    let greeting = try? storage?.object(forKey: "greeting")
    

Competitor Comparisons

1,153

RxSwift extension for RealmSwift's types

Pros of RxRealm

  • Seamless integration with RxSwift, allowing for reactive programming with Realm databases
  • Provides reactive extensions for Realm objects and collections
  • Simplifies handling of asynchronous database operations in a reactive manner

Cons of RxRealm

  • Limited to Realm database, not suitable for other storage types
  • Requires knowledge of both RxSwift and Realm, which may increase the learning curve
  • May have performance overhead due to reactive wrappers

Code Comparison

RxRealm:

let realm = try! Realm()
let dogs = realm.objects(Dog.self)

Observable.collection(from: dogs)
    .subscribe(onNext: { changes in
        // Handle changes
    })
    .disposed(by: disposeBag)

Cache:

let storage = try! Storage<String, String>(
    diskConfig: DiskConfig(name: "MyCache"),
    memoryConfig: MemoryConfig(),
    transformer: TransformerFactory.forCodable(ofType: String.self)
)

storage.async.setObject("value", forKey: "key") { result in
    // Handle result
}

Summary

RxRealm is specifically designed for reactive programming with Realm databases, offering seamless integration with RxSwift. It simplifies handling of asynchronous database operations in a reactive manner. However, it's limited to Realm and requires knowledge of both RxSwift and Realm.

Cache, on the other hand, is a more general-purpose caching library that supports various storage types and doesn't require knowledge of specific reactive programming paradigms. It may be more suitable for projects that don't use Realm or RxSwift, or require more flexibility in storage options.

Realm is a mobile database: a replacement for Core Data & SQLite

Pros of Realm

  • Offers a more comprehensive database solution with advanced querying capabilities
  • Provides real-time synchronization and multi-user collaboration features
  • Supports multiple platforms (iOS, Android, web) with a consistent API

Cons of Realm

  • Steeper learning curve due to its more complex architecture
  • Larger footprint and potentially higher resource usage
  • May be overkill for simple caching needs

Code Comparison

Cache:

let cache = Cache<String, Int>(name: "MyCache")
try? cache.set(value: 42, forKey: "answer")
let value = try? cache.object(forKey: "answer")

Realm:

let realm = try! Realm()
try! realm.write {
    realm.add(MyObject(value: ["answer": 42]))
}
let value = realm.objects(MyObject.self).filter("key == 'answer'").first?.value

Summary

Cache is a lightweight caching solution ideal for simple key-value storage, while Realm is a full-featured database system. Cache is easier to implement for basic caching needs, but Realm offers more advanced features for complex data management and synchronization across devices. The choice between the two depends on the specific requirements of your project, with Cache being more suitable for straightforward caching tasks and Realm for more comprehensive data persistence and manipulation needs.

A type-safe, Swift-language layer over SQLite3.

Pros of SQLite.swift

  • Direct SQLite database access, offering full SQL functionality and complex queries
  • Lightweight and efficient for large datasets and relational data
  • Type-safe query interface with compile-time checks

Cons of SQLite.swift

  • Steeper learning curve, especially for developers unfamiliar with SQL
  • Requires more manual management of data structures and relationships
  • Less abstraction, potentially leading to more verbose code for simple operations

Code Comparison

SQLite.swift:

let db = try Connection("path/to/db.sqlite3")
try db.run("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)")
try db.run("INSERT INTO users (name) VALUES (?)", "Alice")
for user in try db.prepare("SELECT * FROM users") {
    print("User: \(user[1])")
}

Cache:

let storage = try! Storage<String, User>(diskConfig: DiskConfig(name: "Users"))
try? storage.setObject(User(name: "Alice"), forKey: "user1")
if let user = try? storage.object(forKey: "user1") {
    print("User: \(user.name)")
}

SQLite.swift provides more control over database operations but requires SQL knowledge, while Cache offers a simpler key-value storage approach with less flexibility but easier implementation for basic caching needs.

A toolkit for SQLite databases, with a focus on application development

Pros of GRDB.swift

  • More comprehensive database solution, offering full SQLite functionality
  • Supports advanced features like migrations, full-text search, and encryption
  • Provides type-safe SQL queries and result access

Cons of GRDB.swift

  • Steeper learning curve due to more complex API and SQL knowledge requirement
  • Potentially overkill for simple caching needs
  • Larger codebase and potential overhead for basic operations

Code Comparison

GRDB.swift example:

try dbQueue.write { db in
    try Player(name: "Arthur", score: 100).insert(db)
}

let players = try dbQueue.read { db in
    try Player.filter(Column("score") > 80).fetchAll(db)
}

Cache example:

try? storage.setObject("Arthur", forKey: "name")
try? storage.setObject(100, forKey: "score")

let name = try? storage.object(ofType: String.self, forKey: "name")
let score = try? storage.object(ofType: Int.self, forKey: "score")

Summary

GRDB.swift is a more powerful and feature-rich database solution, offering full SQLite functionality with type-safe queries and advanced features. However, it comes with a steeper learning curve and may be excessive for simple caching needs. Cache, on the other hand, provides a simpler API focused on key-value storage, making it easier to use for basic caching tasks but lacking in advanced database features.

Simple Swift wrapper for Keychain that works on iOS, watchOS, tvOS and macOS.

Pros of KeychainAccess

  • Specialized for secure storage of sensitive data in iOS Keychain
  • Simple and intuitive API for Keychain operations
  • Supports synchronization across devices via iCloud Keychain

Cons of KeychainAccess

  • Limited to Keychain storage, not suitable for general-purpose caching
  • Lacks advanced features like expiration policies and size limits
  • iOS/macOS specific, not cross-platform

Code Comparison

KeychainAccess:

let keychain = Keychain(service: "com.example.app")
keychain["username"] = "john_doe"
let username = keychain["username"]

Cache:

let storage = try? Storage<String, String>(
    diskConfig: DiskConfig(name: "MyCache"),
    memoryConfig: MemoryConfig(),
    transformer: TransformerFactory.forCodable(ofType: String.self)
)
try? storage?.setObject("john_doe", forKey: "username")
let username = try? storage?.object(forKey: "username")

Summary

KeychainAccess is focused on secure storage in iOS Keychain, offering a simple API for sensitive data. Cache is a more general-purpose caching solution with support for both memory and disk storage, as well as additional features like expiration policies. KeychainAccess is ideal for storing small amounts of sensitive data, while Cache is better suited for larger datasets and general caching needs across platforms.

The better way to deal with JSON data in Swift.

Pros of SwiftyJSON

  • Specialized for JSON parsing and manipulation in Swift
  • Simpler syntax for accessing JSON data
  • Extensive error handling for JSON-related operations

Cons of SwiftyJSON

  • Limited to JSON data only, not a general-purpose caching solution
  • Lacks disk persistence capabilities
  • No built-in support for data expiration or automatic cleanup

Code Comparison

SwiftyJSON:

let json = JSON(data: dataFromNetworking)
if let name = json["user"]["name"].string {
    // Do something with name
}

Cache:

try? storage.setObject(user, forKey: "user")
if let user: User = try? storage.object(forKey: "user") {
    // Do something with user
}

Key Differences

  • SwiftyJSON focuses on JSON parsing and manipulation, while Cache is a general-purpose caching solution
  • Cache provides disk persistence and memory caching, SwiftyJSON operates on in-memory JSON data
  • SwiftyJSON offers a more intuitive syntax for working with JSON structures
  • Cache supports various data types and custom objects, not limited to JSON

Use Cases

SwiftyJSON is ideal for projects heavily reliant on JSON data processing, especially when working with complex JSON structures. Cache is better suited for applications requiring persistent storage, caching of various data types, and management of cached data lifecycle.

Convert Figma logo designs to code with AI

Visual Copilot

Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

README

Cache

CI Status Version Carthage Compatible License Platform Documentation Swift

Table of Contents

Description

Cache Icon

Cache doesn't claim to be unique in this area, but it's not another monster library that gives you a god's power. It does nothing but caching, but it does it well. It offers a good public API with out-of-box implementations and great customization possibilities. Cache utilizes Codable in Swift 4 to perform serialization.

Read the story here Open Source Stories: From Cachable to Generic Storage in Cache

Key features

  • Work with Swift 4 Codable. Anything conforming to Codable will be saved and loaded easily by Storage.
  • Hybrid with memory and disk storage.
  • Many options via DiskConfig and MemoryConfig.
  • Support expiry and clean up of expired objects.
  • Thread safe. Operations can be accessed from any queue.
  • Sync by default. Also support Async APIs.
  • Extensive unit test coverage and great documentation.
  • iOS, tvOS and macOS support.

Usage

Storage

Cache is built based on Chain-of-responsibility pattern, in which there are many processing objects, each knows how to do 1 task and delegates to the next one, so can you compose Storages the way you like.

For now the following Storage are supported

  • MemoryStorage: save object to memory.
  • DiskStorage: save object to disk.
  • HybridStorage: save object to memory and disk, so you get persistented object on disk, while fast access with in memory objects.
  • SyncStorage: blocking APIs, all read and write operations are scheduled in a serial queue, all sync manner.
  • AsyncStorage: non-blocking APIs, operations are scheduled in an internal queue for serial processing. No read and write should happen at the same time.

Although you can use those Storage at your discretion, you don't have to. Because we also provide a convenient Storage which uses HybridStorage under the hood, while exposes sync and async APIs through SyncStorage and AsyncStorage.

All you need to do is to specify the configuration you want with DiskConfig and MemoryConfig. The default configurations are good to go, but you can customise a lot.

let diskConfig = DiskConfig(name: "Floppy")
let memoryConfig = MemoryConfig(expiry: .never, countLimit: 10, totalCostLimit: 10)

let storage = try? Storage(
  diskConfig: diskConfig,
  memoryConfig: memoryConfig,
  transformer: TransformerFactory.forCodable(ofType: User.self) // Storage<String, User>
)

Generic, Type safety and Transformer

All Storage now are generic by default, so you can get a type safety experience. Once you create a Storage, it has a type constraint that you don't need to specify type for each operation afterwards.

If you want to change the type, Cache offers transform functions, look for Transformer and TransformerFactory for built-in transformers.

let storage: Storage<String, User> = ...
storage.setObject(superman, forKey: "user")

let imageStorage = storage.transformImage() // Storage<String, UIImage>
imageStorage.setObject(image, forKey: "image")

let stringStorage = storage.transformCodable(ofType: String.self) // Storage<String, String>
stringStorage.setObject("hello world", forKey: "string")

Each transformation allows you to work with a specific type, however the underlying caching mechanism remains the same, you're working with the same Storage, just with different type annotation. You can also create different Storage for each type if you want.

Transformer is necessary because the need of serialising and deserialising objects to and from Data for disk persistency. Cache provides default Transformer for Data, Codable and UIImage/NSImage

Codable types

Storage supports any objects that conform to Codable protocol. You can make your own things conform to Codable so that can be saved and loaded from Storage.

The supported types are

  • Primitives like Int, Float, String, Bool, ...
  • Array of primitives like [Int], [Float], [Double], ...
  • Set of primitives like Set<String>, Set<Int>, ...
  • Simply dictionary like [String: Int], [String: String], ...
  • Date
  • URL
  • Data

Error handling

Error handling is done via try catch. Storage throws errors in terms of StorageError.

public enum StorageError: Error {
  /// Object can not be found
  case notFound
  /// Object is found, but casting to requested type failed
  case typeNotMatch
  /// The file attributes are malformed
  case malformedFileAttributes
  /// Can't perform Decode
  case decodingFailed
  /// Can't perform Encode
  case encodingFailed
  /// The storage has been deallocated
  case deallocated
  /// Fail to perform transformation to or from Data
  case transformerFail
}

There can be errors because of disk problem or type mismatch when loading from storage, so if want to handle errors, you need to do try catch

do {
  let storage = try Storage(diskConfig: diskConfig, memoryConfig: memoryConfig)
} catch {
  print(error)
}

Configuration

Here is how you can play with many configuration options

let diskConfig = DiskConfig(
  // The name of disk storage, this will be used as folder name within directory
  name: "Floppy",
  // Expiry date that will be applied by default for every added object
  // if it's not overridden in the `setObject(forKey:expiry:)` method
  expiry: .date(Date().addingTimeInterval(2*3600)),
  // Maximum size of the disk cache storage (in bytes)
  maxSize: 10000,
  // Where to store the disk cache. If nil, it is placed in `cachesDirectory` directory.
  directory: try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask,
    appropriateFor: nil, create: true).appendingPathComponent("MyPreferences"),
  // Data protection is used to store files in an encrypted format on disk and to decrypt them on demand
  protectionType: .complete
)
let memoryConfig = MemoryConfig(
  // Expiry date that will be applied by default for every added object
  // if it's not overridden in the `setObject(forKey:expiry:)` method
  expiry: .date(Date().addingTimeInterval(2*60)),
  /// The maximum number of objects in memory the cache should hold
  countLimit: 50,
  /// The maximum total cost that the cache can hold before it starts evicting objects
  totalCostLimit: 0
)

On iOS, tvOS we can also specify protectionType on DiskConfig to add a level of security to files stored on disk by your app in the app’s container. For more information, see FileProtectionType

Sync APIs

Storage is sync by default and is thread safe, you can access it from any queues. All Sync functions are constrained by StorageAware protocol.

// Save to storage
try? storage.setObject(10, forKey: "score")
try? storage.setObject("Oslo", forKey: "my favorite city", expiry: .never)
try? storage.setObject(["alert", "sounds", "badge"], forKey: "notifications")
try? storage.setObject(data, forKey: "a bunch of bytes")
try? storage.setObject(authorizeURL, forKey: "authorization URL")

// Load from storage
let score = try? storage.object(forKey: "score")
let favoriteCharacter = try? storage.object(forKey: "my favorite city")

// Check if an object exists
let hasFavoriteCharacter = try? storage.objectExists(forKey: "my favorite city")

// Remove an object in storage
try? storage.removeObject(forKey: "my favorite city")

// Remove all objects
try? storage.removeAll()

// Remove expired objects
try? storage.removeExpiredObjects()

Entry

There is time you want to get object together with its expiry information and meta data. You can use Entry

let entry = try? storage.entry(forKey: "my favorite city")
print(entry?.object)
print(entry?.expiry)
print(entry?.meta)

meta may contain file information if the object was fetched from disk storage.

Custom Codable

Codable works for simple dictionary like [String: Int], [String: String], ... It does not work for [String: Any] as Any is not Codable conformance, it will raise fatal error at runtime. So when you get json from backend responses, you need to convert that to your custom Codable objects and save to Storage instead.

struct User: Codable {
  let firstName: String
  let lastName: String
}

let user = User(fistName: "John", lastName: "Snow")
try? storage.setObject(user, forKey: "character")

Async APIs

In async fashion, you deal with Result instead of try catch because the result is delivered at a later time, in order to not block the current calling queue. In the completion block, you either have value or error.

You access Async APIs via storage.async, it is also thread safe, and you can use Sync and Async APIs in any order you want. All Async functions are constrained by AsyncStorageAware protocol.

storage.async.setObject("Oslo", forKey: "my favorite city") { result in
  switch result {
    case .success:
      print("saved successfully")
    case .failure(let error):
      print(error)
  }
}

storage.async.object(forKey: "my favorite city") { result in
  switch result {
    case .success(let city):
      print("my favorite city is \(city)")
    case .failure(let error):
      print(error)
  }
}

storage.async.objectExists(forKey: "my favorite city") { result in
  if case .success(let exists) = result, exists {
    print("I have a favorite city")
  }
}

storage.async.removeAll() { result in
  switch result {
    case .success:
      print("removal completes")
    case .failure(let error):
      print(error)
  }
}

storage.async.removeExpiredObjects() { result in
  switch result {
    case .success:
      print("removal completes")
    case .failure(let error):
      print(error)
  }
}

Swift Concurrency

do {
  try await storage.async.setObject("Oslo", forKey: "my favorite city")
  print("saved successfully")
} catch {
  print(error)
}

do {
  let city = try await storage.async.object(forKey: "my favorite city")
  print("my favorite city is \(city)")
} catch {
  print(error)
}

do {
  let exists = try await storage.async.objectExists(forKey: "my favorite city")
  if exists {
    print("I have a favorite city")
  }
} catch {}

do {
  try await storage.async.remoeAll()
  print("removal completes")
} catch {
  print(error)
}

do {
  try await storage.async.removeExpiredObjects()
  print("removal completes")
} catch {
  print(error)
}

Expiry date

By default, all saved objects have the same expiry as the expiry you specify in DiskConfig or MemoryConfig. You can overwrite this for a specific object by specifying expiry for setObject

// Default expiry date from configuration will be applied to the item
try? storage.setObject("This is a string", forKey: "string")

// A given expiry date will be applied to the item
try? storage.setObject(
  "This is a string",
  forKey: "string",
  expiry: .date(Date().addingTimeInterval(2 * 3600))
)

// Clear expired objects
storage.removeExpiredObjects()

Observations

Storage allows you to observe changes in the cache layer, both on a store and a key levels. The API lets you pass any object as an observer, while also passing an observation closure. The observation closure will be removed automatically when the weakly captured observer has been deallocated.

Storage observations

// Add observer
let token = storage.addStorageObserver(self) { observer, storage, change in
  switch change {
  case .add(let key):
    print("Added \(key)")
  case .remove(let key):
    print("Removed \(key)")
  case .removeAll:
    print("Removed all")
  case .removeExpired:
    print("Removed expired")
  }
}

// Remove observer
token.cancel()

// Remove all observers
storage.removeAllStorageObservers()

Key observations

let key = "user1"

let token = storage.addObserver(self, forKey: key) { observer, storage, change in
  switch change {
  case .edit(let before, let after):
    print("Changed object for \(key) from \(String(describing: before)) to \(after)")
  case .remove:
    print("Removed \(key)")
  }
}

// Remove observer by token
token.cancel()

// Remove observer for key
storage.removeObserver(forKey: key)

// Remove all observers
storage.removeAllKeyObservers()

Handling JSON response

Most of the time, our use case is to fetch some json from backend, display it while saving the json to storage for future uses. If you're using libraries like Alamofire or Malibu, you mostly get json in the form of dictionary, string, or data.

Storage can persist String or Data. You can even save json to Storage using JSONArrayWrapper and JSONDictionaryWrapper, but we prefer persisting the strong typed objects, since those are the objects that you will use to display in UI. Furthermore, if the json data can't be converted to strongly typed objects, what's the point of saving it ? 😉

You can use these extensions on JSONDecoder to decode json dictionary, string or data to objects.

let user = JSONDecoder.decode(jsonString, to: User.self)
let cities = JSONDecoder.decode(jsonDictionary, to: [City].self)
let dragons = JSONDecoder.decode(jsonData, to: [Dragon].self)

This is how you perform object converting and saving with Alamofire

Alamofire.request("https://gameofthrones.org/mostFavoriteCharacter").responseString { response in
  do {
    let user = try JSONDecoder.decode(response.result.success, to: User.self)
    try storage.setObject(user, forKey: "most favorite character")
  } catch {
    print(error)
  }
}

What about images

If you want to load image into UIImageView or NSImageView, then we also have a nice gift for you. It's called Imaginary and uses Cache under the hood to make your life easier when it comes to working with remote images.

Installation

Cocoapods

Cache is available through CocoaPods. To install it or update it, use the following line to your Podfile:

pod 'Cache', :git => 'https://github.com/hyperoslo/Cache.git'

Carthage

Cache is also available through Carthage. To install just write into your Cartfile:

github "hyperoslo/Cache"

You also need to add SwiftHash.framework in your copy-frameworks script.

Author

  • Hyper made this with ❤️
  • Inline MD5 implementation from SwiftHash

Contributing

We would love you to contribute to Cache, check the CONTRIBUTING file for more info.

License

Cache is available under the MIT license. See the LICENSE file for more info.