Droste
Introduction
Droste is a lightweight composable caching library which leverages RxSwift's Observable
for its API.
Example usage
Using out of the box caches
import Droste
let aDisposeBag = DisposeBag()
let url = URL(string: "https://en.wikipedia.org/w/api.php?action=query&prop=revisions&rvprop=content&rvsection=0&titles=pizza&format=json")!
Caches
.sharedJSONCache
.get(url)
.subscribe(onNext: { jsonObject in
//use jsonObject
}).disposed(by: aDisposeBag)
Or composing your own cache
import Droste
let aDisposeBag = DisposeBag()
let url = URL(string: "https://en.wikipedia.org/w/api.php?action=query&prop=revisions&rvprop=content&rvsection=0&titles=pizza&format=json")!
let networkFetcher = NetworkFetcher()
.mapKeys { (url: URL) -> URLRequest in //map url to urlrequest
return URLRequest(url: url)
}
let diskCache = DiskCache<URL, NSData>()
let ramCache = RamCache<URL, NSData>()
let dataCache = ramCache + (diskCache + networkFetcher).reuseInFlight()
let jsonCache = dataCache
.mapValues(f: { (data) -> AnyObject in
//convert NSData to json object
return try JSONSerialization.jsonObject(with: data as Data, options: [.allowFragments]) as AnyObject
}, fInv: { (object) -> NSData in
//convert json object to NSData
return try JSONSerialization.data(withJSONObject: object, options: []) as NSData
})
jsonCache
.get(url)
.subscribe(onNext: { jsonObject in
//use jsonObject
}).disposed(by: aDisposeBag)
Features
- Out of the box support for UIImage, JSON, NSData
- Expirable caches
- Combine caches easily (e.g. ram + disk + network)
- Because
.get
returns aCancelable
when you subscribe, you can cancel the request anytime - Combine different type of caches by mapping the value of caches using
.mapValues
and the key using.mapKeys
- When a cache succeds, informs all the higher level caches to set the new value
- Forward request to a lower level cache even if the value exists by using
.forwardRequest
operator - Create conditional combining of caches in runtime by using the
.switchCache
operator - If the request is expensive you can consolidate requests that have the same key and the first request is not done yet, using the
.reuseInFlight
operator - Skip a cache at runtime depending on a condition, using the
.skipWhile
operator - Salient .get if you want the lack of a value to be treated as error
- Proper error handling through
Observable
chain sequence
Out of the box
We provide some singletons of caches that are ready for use.
Caches.sharedJSONCache
takes URL and returns AnyObject which could either be an Array or a Dictionary depending on the endpoint's responseCaches.sharedDataCache
takes URL and returns NSDataCaches.sharedImageCache
takes URL and returns UIImage
Compose caches
You can compose different types of caches, such as a composition of ram cache with a disk cache backed by a network fetcher.
To compose two or more caches, you can use .compose
or +
operator.
Example
let ramCache = RamCache<URL, NSData>()
let diskCache = DiskCache<URL, NSData>()
//First hit ram cache and in case of failure, hit the disk cache
let ramDiskCache = ramCache.compose(other: diskCache)
//Same composition example using the the overloaded `+` operator
let ramDiskCache = ramCache + diskCache
Expirable caches
You can set expiry in each cache that supports it. DiskCache and RamCache both have out of the box support! You can set an expiry in the following ways
// seconds from now
let ramCache = RamCache<URL, NSData>().expires(at: .seconds(30))
// or by explicitly setting a date
let diskCache = DiskCache<URL, NSData>().expires(at: .date(Date(timeIntervalSince1970: 1516045540)))
Refreshing a resource after 5 minutes in both ram and disk cache.
let ramCache = RamCache<URL, NSData>().expires(at: .seconds(600))
let diskCache = DiskCache<URL, NSData>().expires(at: .seconds(600))
let dataCache = ramCache + diskCache + networkFetcher
Cancel Requests
The fact that Droste leverages RxSwift's power, gives us some extra free functionality, such as canceling on-going requests.
DisposeBag
Example with var aDisposeBag = DisposeBag()
let diskCache = DiskCache<URL, NSData>()
let networkFetcher = NetworkFetcher<URL, NSData>()
let diskNetworkCache = diskCache + networkFetcher
diskNetworkCache
.get("a url")
.subscribe(onNext: { response in
}).disposed(by: aDisposeBag)
//...later
aDisposeBag = DisposeBag() // this line will cancel the on-going request
flatMapLatest
Example with var aDisposeBag = DisposeBag()
let diskCache = DiskCache<URL, NSData>()
let networkFetcher = NetworkFetcher<URL, NSData>()
let diskNetworkCache = diskCache + networkFetcher
//Each time the user input emits a new value, it cancels the previous request in cache
someUserInput
.flatMapLatest{ userInput in
let key = exampleKey(from: userInput)
return diskNetworkCache.get(key)
}
.subscribe(onNext: { response in
}).disposed(by: aDisposeBag)
Map Values and Keys
Different Caches may know about different types of values and keys.
For example, you may have a RAM cache which uses a String
type key and returns a UIImage
type and a network fetcher (which is a type of cache) that uses URL as a key and responds with NSData.
If you would like to combine those two Caches you could not do it directly because their types do not match.
You can map the values and key of a cache using mapValues
and mapKeys
.
Example
let networkFetcher = NetworkFetcher() //Value = NSData, Key = URLRequest
let ramCache = RamCache<String, UIImage>() //Value = UIImage, Key = String
let imageNetworkFetcher = networkFetcher //Value = UIImage, Key = String
.mapKeys { (urlString: String) -> URLRequest in
let url = URL(string: urlString)!
return URLRequest(url: url)
}
.mapValues(
f: { (data) -> UIImage in
return UIImage(data: data as Data)!
}) { (image) -> NSData in
return UIImagePNGRepresentation(image) as! NSData
}
let imageRamNetworkCache = ramCache + imageNetworkFetcher //Value = UIImage, Key = String
imageRamNetworkCache.get("http://an.image.url.png")
.subscribe(onNext: { image in
//in a view controller context
self.imageView.image = image
}).disposed(by: disposeBag)
More examples here
Forward request
In a scenario that you have composed two Caches together and you want the right hand side cache to always hit the network, regardless if the first cache has succeded or not, you can use the forward request operator. By doing this, you are able to get the value from the first cache (if it exists) and followning the value from the second cache (if it exists). Also, note that the .forwardRequest
operator, does not provide any guarantees on the order of the emissions to be aligned with the order of the composition.
Example
Let's assume that you have a screen which you want to load instantly and present a cached result for a short period of time, but always update it when the network request succeeds.
let screenCache = diskCache.forwardRequest() + networkFetcher
screenCache
.get("withKey")
.subscribe(onNext: { screenViewModel
self.viewModel = screenViewModel //if disk cache has a value this will be called twice
})
Switch cache
You can switch between Caches in runtime depending on the key used.
Example
let cacheA = ...
let cacheB = ...
let cache = switchCache(cacheA: cacheA, cacheB: cacheB) { (key) -> CacheSwitchResult in
if key == "Hello" {
return .cacheA
}
if key == "World" {
return .cacheB
}
}
cache.get("Hello") //this will use cacheA
cache.get("World") //this will use cacheB
Reuse in flight
When you want to get an expensive resource from a cache/fetcher (e.g. NetworkFetcher
), you can use .reuseInFlight
operator to consolidate all requests that have the same key provided, when there is already an in-flight request for the same key. An example of this scenario is, a chat app that presents the same avatar multiple times on the screen and each avatar makes its own request from the cache. Using this operator means that only one request will be made and any subsequent requests will share the response of the first one.
Example
let cache = memoryCache + (diskCache + networkFetcher).reuseInFlight()
cache.get("profileImage")
.subscribe(onNext: { image in
avatar1.image = image
})
//a short while after, before the first request finished
cache.get("profileImage")
.subscribe(onNext: { image in
avatar2.image = image //this will get the same response as the above without making a second request
}
Notes
- Keys must be
Hashable
. - This operator is thread safe, meaning that you can dispatch the same request from multiple queues and it will work without any unexpected behaviors.
Skip while
With .skipWhile
operator you can, at runtime, to skip the cache depending on a given condition.
Example
let updateFromNetwork = true
let conditionedDiskCache = diskCache.skipWhile { key in
let shouldSkip = (key == "a" && updateFromNetwork)
return Observable(shouldSkip)
}
let cache = conditionedDiskCache + networkFetcher
cache.get("a")
.subscribe(onNext: { response in
})
Salient get
We have two .get
operators which have a slightly different behavior.
func get(_ key: Key) -> Observable<Value?>
Optional return value
Using this operator, means that, if the cache does not have a value and has not thrown an error internally, then it will return a nil
value. All thrown errors will propagate as expected.
func get(_ key: Key) -> Observable<Value>
Non-optional return value
Using this operator, means that, if the cache does not have a value, then it will throw a CacheFetchError.valueNotFound
error. This operator does not change the expected behavior of other thrown errors.
Requirements
- iOS 9.0+
- Xcode 8.0+
Getting involved
- If you want to contribute please feel free to submit PRs.
- If you have a feature request please open an issue.
- If you found a bug or need help please check older issues, FAQ and threads on StackOverflow (Tag 'Droste') before submitting an issue.
Before contributing check the CONTRIBUTING file for more info.
If you use Droste in your app we would love to hear about it! Drop us a line on Twitter.
Examples
Follow these 3 steps to run the example project:
- Clone Droste repository
- Open Example/Example.xcworkspace workspace
- Run Example project
You can also experiment and learn the using the Droste Playground, contained in Example/Example.workspace under Droste project.
Installation
CocoaPods
To install Droste, simply add the following line to your Podfile:
pod 'Droste', '~> 0.2.0'
Carthage
To install Droste, simply add the following line to your Cartfile:
github "gtsifrikas/Droste" ~> 0.2.0
Tasks for version 1.0
- 100% coverage
- Support macOS
- Support linux
- Update README to include custom Cache creation.
- Come up with a api for TTL and support it in DiskCache and RamCache.
Author
Changelog
See CHANGELOG.