TestsTested | ✓ |
LangLanguage | SwiftSwift |
License | MIT |
ReleasedLast Release | Oct 2016 |
SPMSupports SPM | ✗ |
Maintained by Victor Albertos, Roberto Frontado.
Swift adaptation from the original RxCache Java version.
The goal of this library is simple: caching your data models like SDWebImage caches your images, with no effort at all.
Every Swift application is a client application, which means it does not make sense to create and maintain a database just for caching data.
Plus, the fact that you have some sort of legendary database for persisting your data does not solves by itself the real challenge: to be able to configure your caching needs in a flexible and simple way.
Inspired by Moya api, RxCache is a reactive caching library for Swift which relies on RxSwift for turning your caching needs into an enum
.
Every enum
value acts as a provider for RxCache, and all of them are managed through observables; they are the fundamental contract between the library and its clients.
When supplying an observable
which contains the data provided by an expensive task -probably a http connection, RxCache determines if it is needed to subscribe to it or instead fetch the data previously cached. This decision is made based on the providers configuration.
So, when supplying an observable
you get your observable cached back, and next time you will retrieve it without the time cost associated with its underlying task.
RxCache is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'RxCache', '~> 0.1.4'
The data model which will be used for RxCache requires to conform with GlossCacheable or OMCacheable protocol
.
Using Gloss
import Gloss
import RxCache
struct Person: Glossy, GlossCacheable {
let name: String
init?(json: JSON) {
guard let name: String = "name" <~~ json else { return nil }
self.name = name
}
func toJSON() -> JSON? {
return jsonify([
"name" ~~> self.name]
)
}
}
Using ObjectMapper
import ObjectMapper
import RxCache
class Person: Mappable, OMCacheable {
var name: String?
required init?(JSON: [String : AnyObject]) {
mapping(Map(mappingType: .FromJSON, JSONDictionary: JSON))
}
func mapping(map: Map) {
name <- map["name"]
}
required init?(_ map: Map) { }
}
If you would like to add another mapping library that converts to and from JSON ask for it or make a PR :-)
enum
providersAfter your model conforms with GlossCacheable
OMCacheable
or protocol
, you can now create an enum
which conforms with the Provider protocol with as much values as needed to create the caching providers:
enum CacheProviders : Provider {
case GetMocks()
case GetMocksWith5MinutesLifeTime()
case GetMocksEvictProvider(evict : Bool)
case GetMocksPaginate(page : Int)
case GetMocksPaginateEvictingPerPage(page : Int, evictByPage: Bool)
case GetMocksPaginateWithFiltersEvictingPerFilter(filter: String, page : String, evictByPage: Bool)
var lifeCache: LifeCache? {
switch self {
case GetMocksWith5MinutesLifeTime:
return LifeCache(duration: 5, timeUnit: LifeCache.TimeUnit.Minutes)
default:
return nil
}
}
var dynamicKey: DynamicKey? {
switch self {
case let GetMocksPaginate(page):
return DynamicKey(dynamicKey: String(page))
case let GetMocksPaginateEvictingPerPage(page, _):
return DynamicKey(dynamicKey: String(page))
default:
return nil
}
}
var dynamicKeyGroup: DynamicKeyGroup? {
switch self {
case let GetMocksPaginateWithFiltersEvictingPerFilter(filter, page, _):
return DynamicKeyGroup(dynamicKey: filter, group: page)
default:
return nil
}
}
var evict: EvictProvider? {
switch self {
case let GetMocksEvictProvider(evict):
return EvictProvider(evict: evict)
case let GetMocksPaginateEvictingPerPage(_, evictByPage):
return EvictDynamicKey(evict: evictByPage)
case let GetMocksPaginateWithFiltersEvictingPerFilter(_, _, evictByPage):
return EvictDynamicKey(evict: evictByPage)
default:
return nil
}
}
}
RxCache provides a set of classes to indicate how the Provider needs to handle the cached data:
enum
Now you can use your enum
providers calling RxCache.Providers.cache()
static method.
It’s a really good practice to organize your data providers in a set of Repository classes where RxCache and Moya work together. Indeed, RxCache is the perfect match for Moya to create a repository of auto-managed-caching data pointing to endpoints.
This would be the MockRepository
:
class MockRepository {
private let providers: RxCache
init() {
providers = RxCache.Providers
}
func getMocks(update: Bool) -> Observable<[Mock]> {
let provider : Provider = CacheProviders.GetMocksEvictProvider(evict: update)
return providers.cache(getExpensiveMocks(), provider: provider)
}
func getMocksPaginate(page: Int, update: Bool) -> Observable<[Mock]> {
let provider : Provider = CacheProviders.GetMocksPaginateEvictingPerPage(page: page, evictByPage: update)
return providers.cache(getExpensiveMocks(), provider: provider)
}
func getMocksWithFiltersPaginate(filter: String, page: Int, update: Bool) -> Observable<[Mock]> {
let provider : Provider = CacheProviders.GetMocksPaginateWithFiltersEvictingPerFilter(filter: filter, page: page, evictByPage: update)
return providers.cache(getExpensiveMocks(), provider: provider)
}
//In a real use case, here is when you build your observable with the expensive operation.
//Or if you are making http calls you can use Moya-RxSwift extensions to get it out of the box.
private func getExpensiveMocks() -> Observable<[Mock]> {
return Observable.just([Mock]())
}
}
Following use cases illustrate some common scenarios which will help to understand the usage of DynamicKey
and DynamicKeyGroup
classes along with evicting scopes.
enum CacheProviders : Provider {
// Mock List
case GetMocks() // Mock List without evicting
case GetMocksEvictProvider(evictProvider : EvictProvider) // Mock List evicting
// Mock List Filtering
case GetMocksFiltered(filter: String) // Mock List filtering without evicting
case GetMocksFilteredEvict(filter: String, evictProvider : EvictProvider) // Mock List filtering evicting
// Mock List Paginated with filters
case GetMocksFilteredPaginate(filterAndPage: String) // Mock List paginated with filters without evicting
case GetMocksFilteredPaginateEvict(filter: String, page: Int, evictProvider : EvictProvider) // Mock List paginated with filters evicting
var lifeCache: LifeCache? {
return nil
}
var dynamicKey: DynamicKey? {
switch self {
case let GetMocksFiltered(filter):
return DynamicKey(dynamicKey: filter)
case let GetMocksFilteredEvict(filter, _):
return DynamicKey(dynamicKey: filter)
case let GetMocksFilteredPaginate(filterAndPage):
return DynamicKey(dynamicKey: filterAndPage)
default:
return nil
}
}
var dynamicKeyGroup: DynamicKeyGroup? {
switch self {
case let GetMocksFilteredPaginateEvict(filter, page, _):
return DynamicKeyGroup(dynamicKey: filter, group: String(page))
default:
return nil
}
}
var evict: EvictProvider? {
switch self {
case let GetMocksFilteredEvict(_, evictProvider):
return evictProvider
case let GetMocksFilteredPaginateEvict(_, _, evictProvider):
return evictProvider
default:
return nil
}
}
}
//Hit observable evicting all mocks
let provider : Provider = CacheProviders.GetMocksEvictProvider(evictProvider: EvictProvider(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
//This lines return an error observable: "EvictDynamicKey was provided but not was provided any DynamicKey"
let provider : Provider = CacheProviders.GetMocksEvictProvider(evictProvider: EvictDynamicKey(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
//Hit observable evicting all mocks using EvictProvider
let provider : Provider = CacheProviders.GetMocksFilteredEvict(filter: "actives", evictProvider: EvictProvider(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
//Hit observable evicting mocks of one filter using EvictDynamicKey
let provider : Provider = CacheProviders.GetMocksFilteredEvict(filter: "actives", evictProvider: EvictDynamicKey(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
//This lines return an error observable: "EvictDynamicKeyGroup was provided but not was provided any Group"
let provider : Provider = CacheProviders.GetMocksFilteredEvict(filter: "actives", evictProvider: EvictDynamicKeyGroup(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
//Hit observable evicting all mocks using EvictProvider
let provider : Provider = CacheProviders.GetMocksFilteredPaginateEvict(filter: "actives", page: 1, evictProvider: EvictProvider(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
//Hit observable evicting all mocks pages of one filter using EvictDynamicKey
let provider : Provider = CacheProviders.GetMocksFilteredPaginateEvict(filter: "actives", page: 1, evictProvider: EvictDynamicKey(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
//Hit observable evicting one page mocks of one filter using EvictDynamicKeyGroup
let provider : Provider = CacheProviders.GetMocksFilteredPaginateEvict(filter: "actives", page: 1, evictProvider: EvictDynamicKeyGroup(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
As you may already notice, the whole point of using DynamicKey
or DynamicKeyGroup
along with Evict
classes is to play with several scopes when evicting objects.
The enum
provider example declare a type which its associated value accepts EvictProvider
in order to be able to concrete more specifics types of EvictProvider
at runtime.
But I have done that for demonstration purposes, you always should narrow the evicting classes to the type which you really need -indeed, you should not expose the Evict
classes as an associated value, instead require a Bool
and hide the implementation detail in your enum
provider.
For the last example, List Paginated with filters, I would narrow the scope to EvictDynamicKey
in production code, because this way I would be able to paginate the filtered mocks and evict them per its filter, triggered by a pull to refresh for instance.
This actionable api offers an easy way to perform write operations using providers. Although write operations could be achieved using the classic api too, it’s much complex and error-prone. Indeed, the Actions class it’s a wrapper around the classic api which play with evicting scopes and lists.
Some actions examples:
let provider = RxProvidersMock.GetMocksEvictCache(evict: false)
Actions<Mock>.with(provider)
.addFirst(Mock())
.addLast(Mock())
// Add a new mock at 5th position
.add({ (position, count) -> Bool in position == 5 }, candidate: Mock())
.evictFirst()
//Evict first element if the cache has already 300 records
.evictFirst { (count) -> Bool in count > 300 }
.evictLast()
//Evict last element if the cache has already 300 records
.evictLast { (count) -> Bool in count > 300 }
//Evict all inactive elements
.evictIterable { (position, count, mock) -> Bool in mock.active == false }
.evictAll()
//Update the mock with id 5
.update({ mock -> Bool in mock.id == 5 },
replace: { mock in
mock.active = true
return mock
})
//Update all inactive mocks
.update({ mock -> Bool in mock.active == false },
replace: { mock in
mock.active = true
return mock
})
.toObservable()
.subscribe()
Every one of the previous actions will be execute only after the composed observable receives a subscription. This way, the underliyng provider cache will be modified its elements without effort at all.
By default, RxCache sets the limit in 300 megabytes, but you can change this value by calling RxCache.Providers.maxMBPersistenceCache
property:
RxCache.Providers.maxMBPersistenceCache = 100
This limit ensure that the disk will no grow up limitless in case you have providers with dynamic keys which values changes dynamically, like filters based on gps location or dynamic filters supplied by your back-end solution.
When this limit is reached, RxCache will not be able to persist in disk new data. That’s why RxCache has an automated process to evict any record when the threshold memory assigned to the persistence layer is close to be reached, even if the record life time has not been fulfilled.
But implementing expirable()
method from Provider
protocol and returning false as value, the objects persisted with this provider will be exclude from the process.
enum CacheProviders : Provider {
case GetNotExpirableMocks()
public func expirable() -> Bool {
switch self {
case GetNotExpirableMocks():
return false
default:
return true
}
}
}
By default, RxCache will return an observable
error if the cached data has expired and the data returned by the observable
loader is null, preventing this way serving data which has been marked as evicted.
You can modify this behaviour, allowing RxCache serving evicted data when the loader has returned nil values, by setting as true the value of useExpiredDataIfLoaderNotAvailable
RxCache.Providers.useExpiredDataIfLoaderNotAvailable = true
RxCache serves the data from one of its three layers:
The policy is very simple:
Víctor Albertos
RxCache is available under the MIT license. See the LICENSE file for more info.