[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-7003":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":9,"htmlUrl":10,"language":11,"languages":10,"totalLinesOfCode":10,"stars":12,"forks":13,"watchers":14,"openIssues":15,"contributorsCount":16,"subscribersCount":16,"size":16,"stars1d":16,"stars7d":17,"stars30d":18,"stars90d":16,"forks30d":16,"starsTrendScore":16,"compositeScore":19,"rankGlobal":10,"rankLanguage":10,"license":20,"archived":21,"fork":21,"defaultBranch":22,"hasWiki":23,"hasPages":21,"topics":24,"createdAt":10,"pushedAt":10,"updatedAt":31,"readmeContent":32,"aiSummary":33,"trendingCount":16,"starSnapshotCount":16,"syncStatus":34,"lastSyncTime":35,"discoverSource":36},7003,"Cache","hyperoslo\u002FCache","hyperoslo",":package: Nothing but Cache.","",null,"Swift",3143,354,45,8,0,1,3,29.65,"Other",false,"master",true,[25,26,27,28,29,30],"cache","cache-storage","disk-cache","ios","memory-cache","swift","2026-06-12 02:01:33","![Cache](https:\u002F\u002Fgithub.com\u002Fhyperoslo\u002FCache\u002Fblob\u002Fmaster\u002FResources\u002FCachePresentation.png)\n\n[![CI Status](https:\u002F\u002Fcircleci.com\u002Fgh\u002Fhyperoslo\u002FCache.png)](https:\u002F\u002Fcircleci.com\u002Fgh\u002Fhyperoslo\u002FCache)\n[![Version](https:\u002F\u002Fimg.shields.io\u002Fcocoapods\u002Fv\u002FCache.svg?style=flat)](http:\u002F\u002Fcocoadocs.org\u002Fdocsets\u002FCache)\n[![Carthage Compatible](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002FCarthage-compatible-4BC51D.svg?style=flat)](https:\u002F\u002Fgithub.com\u002FCarthage\u002FCarthage)\n[![License](https:\u002F\u002Fimg.shields.io\u002Fcocoapods\u002Fl\u002FCache.svg?style=flat)](http:\u002F\u002Fcocoadocs.org\u002Fdocsets\u002FCache)\n[![Platform](https:\u002F\u002Fimg.shields.io\u002Fcocoapods\u002Fp\u002FCache.svg?style=flat)](http:\u002F\u002Fcocoadocs.org\u002Fdocsets\u002FCache)\n[![Documentation](https:\u002F\u002Fimg.shields.io\u002Fcocoapods\u002Fmetrics\u002Fdoc-percent\u002FCache.svg?style=flat)](http:\u002F\u002Fcocoadocs.org\u002Fdocsets\u002FCache)\n![Swift](https:\u002F\u002Fimg.shields.io\u002Fbadge\u002F%20in-swift%205-orange.svg)\n\n## Table of Contents\n\n* [Description](#description)\n* [Key features](#key-features)\n* [Usage](#usage)\n  * [Storage](#storage)\n  * [Configuration](#configuration)\n  * [Sync APIs](#sync-apis)\n  * [Async APIs](#async-apis)\n  * [Expiry date](#expiry-date)\n* [Observations](#observations)\n  * [Storage observations](#storage-observations)\n  * [Key observations](#key-observations)\n* [Handling JSON response](#handling-json-response)\n* [What about images?](#what-about-images)\n* [Installation](#installation)\n* [Author](#author)\n* [Contributing](#contributing)\n* [License](#license)\n\n## Description\n\n\u003Cimg src=\"https:\u002F\u002Fgithub.com\u002Fhyperoslo\u002FCache\u002Fblob\u002Fmaster\u002FResources\u002FCacheIcon.png\" alt=\"Cache Icon\" align=\"right\" \u002F>\n\n**Cache** doesn't claim to be unique in this area, but it's not another monster\nlibrary that gives you a god's power. It does nothing but caching, but it does it well. It offers a good public API\nwith out-of-box implementations and great customization possibilities. `Cache` utilizes `Codable` in Swift 4 to perform serialization.\n\nRead the story here [Open Source Stories: From Cachable to Generic Storage in Cache](https:\u002F\u002Fmedium.com\u002Fhyperoslo\u002Fopen-source-stories-from-cachable-to-generic-storage-in-cache-418d9a230d51)\n\n## Key features\n\n- [x] Work with Swift 4 `Codable`. Anything conforming to `Codable` will be saved and loaded easily by `Storage`.\n- [x] Hybrid with memory and disk storage.\n- [X] Many options via `DiskConfig` and `MemoryConfig`.\n- [x] Support `expiry` and clean up of expired objects.\n- [x] Thread safe. Operations can be accessed from any queue.\n- [x] Sync by default. Also support Async APIs.\n- [x] Extensive unit test coverage and great documentation.\n- [x] iOS, tvOS and macOS support.\n\n## Usage\n\n### Storage\n\n`Cache` is built based on [Chain-of-responsibility pattern](https:\u002F\u002Fen.wikipedia.org\u002Fwiki\u002FChain-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.\n\nFor now the following Storage are supported\n\n- `MemoryStorage`: save object to memory.\n- `DiskStorage`: save object to disk.\n- `HybridStorage`: save object to memory and disk, so you get persistented object on disk, while fast access with in memory objects.\n- `SyncStorage`: blocking APIs, all read and write operations are scheduled in a serial queue, all sync manner.\n- `AsyncStorage`: non-blocking APIs, operations are scheduled in an internal queue for serial processing. No read and write should happen at the same time.\n\nAlthough 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`.\n\nAll 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.\n\n\n```swift\nlet diskConfig = DiskConfig(name: \"Floppy\")\nlet memoryConfig = MemoryConfig(expiry: .never, countLimit: 10, totalCostLimit: 10)\n\nlet storage = try? Storage(\n  diskConfig: diskConfig,\n  memoryConfig: memoryConfig,\n  transformer: TransformerFactory.forCodable(ofType: User.self) \u002F\u002F Storage\u003CString, User>\n)\n```\n\n### Generic, Type safety and Transformer\n\nAll `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.\n\nIf you want to change the type, `Cache` offers `transform` functions, look for `Transformer` and `TransformerFactory` for built-in transformers.\n\n```swift\nlet storage: Storage\u003CString, User> = ...\nstorage.setObject(superman, forKey: \"user\")\n\nlet imageStorage = storage.transformImage() \u002F\u002F Storage\u003CString, UIImage>\nimageStorage.setObject(image, forKey: \"image\")\n\nlet stringStorage = storage.transformCodable(ofType: String.self) \u002F\u002F Storage\u003CString, String>\nstringStorage.setObject(\"hello world\", forKey: \"string\")\n```\n\nEach 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.\n\n`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\u002FNSImage`\n\n#### Codable types\n\n`Storage` supports any objects that conform to [Codable](https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fswift\u002Fcodable) protocol. You can [make your own things conform to Codable](https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Ffoundation\u002Farchives_and_serialization\u002Fencoding_and_decoding_custom_types) so that can be saved and loaded from `Storage`.\n\nThe supported types are\n\n- Primitives like `Int`, `Float`, `String`, `Bool`, ...\n- Array of primitives like `[Int]`, `[Float]`, `[Double]`, ...\n- Set of primitives like `Set\u003CString>`, `Set\u003CInt>`, ...\n- Simply dictionary like `[String: Int]`, `[String: String]`, ...\n- `Date`\n- `URL`\n- `Data`\n\n#### Error handling\n\nError handling is done via `try catch`. `Storage` throws errors in terms of `StorageError`.\n\n```swift\npublic enum StorageError: Error {\n  \u002F\u002F\u002F Object can not be found\n  case notFound\n  \u002F\u002F\u002F Object is found, but casting to requested type failed\n  case typeNotMatch\n  \u002F\u002F\u002F The file attributes are malformed\n  case malformedFileAttributes\n  \u002F\u002F\u002F Can't perform Decode\n  case decodingFailed\n  \u002F\u002F\u002F Can't perform Encode\n  case encodingFailed\n  \u002F\u002F\u002F The storage has been deallocated\n  case deallocated\n  \u002F\u002F\u002F Fail to perform transformation to or from Data\n  case transformerFail\n}\n```\n\nThere 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`\n\n```swift\ndo {\n  let storage = try Storage(diskConfig: diskConfig, memoryConfig: memoryConfig)\n} catch {\n  print(error)\n}\n```\n\n### Configuration\n\nHere is how you can play with many configuration options\n\n```swift\nlet diskConfig = DiskConfig(\n  \u002F\u002F The name of disk storage, this will be used as folder name within directory\n  name: \"Floppy\",\n  \u002F\u002F Expiry date that will be applied by default for every added object\n  \u002F\u002F if it's not overridden in the `setObject(forKey:expiry:)` method\n  expiry: .date(Date().addingTimeInterval(2*3600)),\n  \u002F\u002F Maximum size of the disk cache storage (in bytes)\n  maxSize: 10000,\n  \u002F\u002F Where to store the disk cache. If nil, it is placed in `cachesDirectory` directory.\n  directory: try! FileManager.default.url(for: .documentDirectory, in: .userDomainMask,\n    appropriateFor: nil, create: true).appendingPathComponent(\"MyPreferences\"),\n  \u002F\u002F Data protection is used to store files in an encrypted format on disk and to decrypt them on demand\n  protectionType: .complete\n)\n```\n\n```swift\nlet memoryConfig = MemoryConfig(\n  \u002F\u002F Expiry date that will be applied by default for every added object\n  \u002F\u002F if it's not overridden in the `setObject(forKey:expiry:)` method\n  expiry: .date(Date().addingTimeInterval(2*60)),\n  \u002F\u002F\u002F The maximum number of objects in memory the cache should hold\n  countLimit: 50,\n  \u002F\u002F\u002F The maximum total cost that the cache can hold before it starts evicting objects\n  totalCostLimit: 0\n)\n```\n\nOn 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](https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Ffoundation\u002Ffileprotectiontype)\n\n### Sync APIs\n\n`Storage` is sync by default and is `thread safe`, you can access it from any queues. All Sync functions are constrained by `StorageAware` protocol.\n\n```swift\n\u002F\u002F Save to storage\ntry? storage.setObject(10, forKey: \"score\")\ntry? storage.setObject(\"Oslo\", forKey: \"my favorite city\", expiry: .never)\ntry? storage.setObject([\"alert\", \"sounds\", \"badge\"], forKey: \"notifications\")\ntry? storage.setObject(data, forKey: \"a bunch of bytes\")\ntry? storage.setObject(authorizeURL, forKey: \"authorization URL\")\n\n\u002F\u002F Load from storage\nlet score = try? storage.object(forKey: \"score\")\nlet favoriteCharacter = try? storage.object(forKey: \"my favorite city\")\n\n\u002F\u002F Check if an object exists\nlet hasFavoriteCharacter = try? storage.objectExists(forKey: \"my favorite city\")\n\n\u002F\u002F Remove an object in storage\ntry? storage.removeObject(forKey: \"my favorite city\")\n\n\u002F\u002F Remove all objects\ntry? storage.removeAll()\n\n\u002F\u002F Remove expired objects\ntry? storage.removeExpiredObjects()\n```\n\n#### Entry\n\nThere is time you want to get object together with its expiry information and meta data. You can use `Entry`\n\n```swift\nlet entry = try? storage.entry(forKey: \"my favorite city\")\nprint(entry?.object)\nprint(entry?.expiry)\nprint(entry?.meta)\n```\n\n`meta` may contain file information if the object was fetched from disk storage.\n\n#### Custom Codable\n\n`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.\n\n```swift\nstruct User: Codable {\n  let firstName: String\n  let lastName: String\n}\n\nlet user = User(fistName: \"John\", lastName: \"Snow\")\ntry? storage.setObject(user, forKey: \"character\")\n```\n\n### Async APIs\n\nIn `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`.\n\nYou 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.\n\n```swift\nstorage.async.setObject(\"Oslo\", forKey: \"my favorite city\") { result in\n  switch result {\n    case .success:\n      print(\"saved successfully\")\n    case .failure(let error):\n      print(error)\n  }\n}\n\nstorage.async.object(forKey: \"my favorite city\") { result in\n  switch result {\n    case .success(let city):\n      print(\"my favorite city is \\(city)\")\n    case .failure(let error):\n      print(error)\n  }\n}\n\nstorage.async.objectExists(forKey: \"my favorite city\") { result in\n  if case .success(let exists) = result, exists {\n    print(\"I have a favorite city\")\n  }\n}\n\nstorage.async.removeAll() { result in\n  switch result {\n    case .success:\n      print(\"removal completes\")\n    case .failure(let error):\n      print(error)\n  }\n}\n\nstorage.async.removeExpiredObjects() { result in\n  switch result {\n    case .success:\n      print(\"removal completes\")\n    case .failure(let error):\n      print(error)\n  }\n}\n```\n\n#### Swift Concurrency\n\n```swift\ndo {\n  try await storage.async.setObject(\"Oslo\", forKey: \"my favorite city\")\n  print(\"saved successfully\")\n} catch {\n  print(error)\n}\n\ndo {\n  let city = try await storage.async.object(forKey: \"my favorite city\")\n  print(\"my favorite city is \\(city)\")\n} catch {\n  print(error)\n}\n\ndo {\n  let exists = try await storage.async.objectExists(forKey: \"my favorite city\")\n  if exists {\n    print(\"I have a favorite city\")\n  }\n} catch {}\n\ndo {\n  try await storage.async.remoeAll()\n  print(\"removal completes\")\n} catch {\n  print(error)\n}\n\ndo {\n  try await storage.async.removeExpiredObjects()\n  print(\"removal completes\")\n} catch {\n  print(error)\n}\n```\n\n### Expiry date\n\nBy 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`\n\n```swift\n\u002F\u002F Default expiry date from configuration will be applied to the item\ntry? storage.setObject(\"This is a string\", forKey: \"string\")\n\n\u002F\u002F A given expiry date will be applied to the item\ntry? storage.setObject(\n  \"This is a string\",\n  forKey: \"string\",\n  expiry: .date(Date().addingTimeInterval(2 * 3600))\n)\n\n\u002F\u002F Clear expired objects\nstorage.removeExpiredObjects()\n```\n\n## Observations\n\n[Storage](#storage) allows you to observe changes in the cache layer, both on\na store and a key levels. The API lets you pass any object as an observer,\nwhile also passing an observation closure. The observation closure will be\nremoved automatically when the weakly captured observer has been deallocated.\n\n## Storage observations\n\n```swift\n\u002F\u002F Add observer\nlet token = storage.addStorageObserver(self) { observer, storage, change in\n  switch change {\n  case .add(let key):\n    print(\"Added \\(key)\")\n  case .remove(let key):\n    print(\"Removed \\(key)\")\n  case .removeAll:\n    print(\"Removed all\")\n  case .removeExpired:\n    print(\"Removed expired\")\n  }\n}\n\n\u002F\u002F Remove observer\ntoken.cancel()\n\n\u002F\u002F Remove all observers\nstorage.removeAllStorageObservers()\n```\n\n## Key observations\n\n```swift\nlet key = \"user1\"\n\nlet token = storage.addObserver(self, forKey: key) { observer, storage, change in\n  switch change {\n  case .edit(let before, let after):\n    print(\"Changed object for \\(key) from \\(String(describing: before)) to \\(after)\")\n  case .remove:\n    print(\"Removed \\(key)\")\n  }\n}\n\n\u002F\u002F Remove observer by token\ntoken.cancel()\n\n\u002F\u002F Remove observer for key\nstorage.removeObserver(forKey: key)\n\n\u002F\u002F Remove all observers\nstorage.removeAllKeyObservers()\n```\n\n## Handling JSON response\n\nMost 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](https:\u002F\u002Fgithub.com\u002FAlamofire\u002FAlamofire) or [Malibu](https:\u002F\u002Fgithub.com\u002Fhyperoslo\u002FMalibu), you mostly get json in the form of dictionary, string, or data.\n\n`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 ? 😉\n\nYou can use these extensions on `JSONDecoder` to decode json dictionary, string or data to objects.\n\n```swift\nlet user = JSONDecoder.decode(jsonString, to: User.self)\nlet cities = JSONDecoder.decode(jsonDictionary, to: [City].self)\nlet dragons = JSONDecoder.decode(jsonData, to: [Dragon].self)\n```\n\nThis is how you perform object converting and saving with `Alamofire`\n\n```swift\nAlamofire.request(\"https:\u002F\u002Fgameofthrones.org\u002FmostFavoriteCharacter\").responseString { response in\n  do {\n    let user = try JSONDecoder.decode(response.result.success, to: User.self)\n    try storage.setObject(user, forKey: \"most favorite character\")\n  } catch {\n    print(error)\n  }\n}\n```\n\n## What about images\n\nIf you want to load image into `UIImageView` or `NSImageView`, then we also have a nice gift for you. It's called [Imaginary](https:\u002F\u002Fgithub.com\u002Fhyperoslo\u002FImaginary) and uses `Cache` under the hood to make your life easier when it comes to working with remote images.\n\n## Installation\n\n### Cocoapods\n\n**Cache** is available through [CocoaPods](http:\u002F\u002Fcocoapods.org). To install\nit or update it, use the following line to your Podfile:\n\n```ruby\npod 'Cache', :git => 'https:\u002F\u002Fgithub.com\u002Fhyperoslo\u002FCache.git'\n\n```\n\n### Carthage\n\n**Cache** is also available through [Carthage](https:\u002F\u002Fgithub.com\u002FCarthage\u002FCarthage).\nTo install just write into your Cartfile:\n\n```ruby\ngithub \"hyperoslo\u002FCache\"\n```\n\nYou also need to add `SwiftHash.framework` in your [copy-frameworks](https:\u002F\u002Fgithub.com\u002FCarthage\u002FCarthage#if-youre-building-for-ios-tvos-or-watchos) script.\n\n## Author\n\n- [Hyper](http:\u002F\u002Fhyper.no) made this with ❤️\n- Inline MD5 implementation from [SwiftHash](https:\u002F\u002Fgithub.com\u002Fonmyway133\u002FSwiftHash)\n\n## Contributing\n\nWe would love you to contribute to **Cache**, check the [CONTRIBUTING](https:\u002F\u002Fgithub.com\u002Fhyperoslo\u002FCache\u002Fblob\u002Fmaster\u002FCONTRIBUTING.md) file for more info.\n\n## License\n\n**Cache** is available under the MIT license. See the [LICENSE](https:\u002F\u002Fgithub.com\u002Fhyperoslo\u002FCache\u002Fblob\u002Fmaster\u002FLICENSE.md) file for more info.\n","Cache 是一个专注于缓存管理的 Swift 库，旨在为 iOS、tvOS 和 macOS 平台提供高效的数据存储解决方案。它支持内存和磁盘混合存储模式，并且能够处理任何符合 `Codable` 协议的对象，使得数据序列化变得简单快捷。此外，Cache 提供了灵活的配置选项，包括过期时间设置及自动清理功能，确保资源的有效利用；同时，其同步与异步 API 的设计保证了跨线程操作的安全性。该库适用于需要优化应用性能、减少网络请求频率或临时保存用户数据的各种场景。",2,"2026-06-11 03:10:04","top_language"]