Easy Swift Futures & Promises
❗️Archived now❗️
Since Apple released Combine
framework, I decide to archive this repo.
You still can use this repo as an example of Future
/Promise
implementation in Swift
.
EasyFutures
Swift implementation of Futures & Promises. You can read more about Futures & Promises in Wikipedia: https://en.wikipedia.org/wiki/Futures_and_promises.
EasyFutures is:
- 100% Swift, 100% test coverage.
- easy to understand.
- type safe (uses Swift generics).
- the avoidance of a "callback hell".
- out of the box errors handling (you don't need to use
do/catch
). - composeable (
map
,flatMap
,filter
,recover
,zip
,andThen
,flatten
). - support sequences (
fold
,traverse
,sequence
). - fully documented.
Documentation
- Full documentation and more examples you can find in Playground (to use playground you should open it in
EasyFutures.xcodeproj
and build EasyFutures framework). - Wiki (full documentation and all examples).
- Unit tests.
- Examples section.
Requirements
- iOS 9.0+
Installation
CocoaPods:
- Add the following line to your
Podfile
:
pod 'EasyFutures'
#for swift less than 4.2 use:
pod 'EasyFutures', '~> 1.1.0'
- Add
use_frameworks!
to yourPodfile
. - Run
pod install
. - Add to files:
import EasyFutures
Examples
Traditional way to write asynchronous code:
func loadChatRoom(_ completion: (_ chat: Chat?, _ error: Error?) -> Void) {}
func loadUser(id: String, _ completion: (_ user: User?, _ error: Error?) -> Void) {}
loadChatRoom { chat, error in
if let chat = chat {
loadUser(id: chat.ownerId) { user, error in
if let user = user {
print(user)
// owner loaded
} else {
// handle error
}
}
} else {
// handle error
}
}
Same logic but with EasyFutures:
func loadChatRoom() -> Future<Chat>
func loadUser(id: String) -> Future<User>
loadChatRoom().flatMap({ chat -> Future<User> in
// loading user
return loadUser(id: chat.ownerId)
}).onSuccess { user in
// user loaded
}.onError { error in
// handle error
}
Future
Future is an object that contains or will contain result
which can be value or error. Usually result gets from some asynchronous process. To receive result you can define onComplete
, onSuccess
, onError
callbacks.
func loadData() -> Future<String>
let future = loadData()
future.onComplete { result in
switch result {
case .value(let value):
// value
case .error(let error):
// error
}
}
future.onSuccess { data in
// value
}.onError { error in
// error
}
Promise
The Promises are used to write functions that returns the Futures. The Promise contains the Future instance and can complete it.
func loadData() -> Future<String> {
// create the promise with String type
let promise = Promise<String>()
// check is url valid
guard let url = URL(string: "https://api.github.com/emojis") else {
// handle error
promise.error(ExampleError.invalidUrl)
return promise.future
}
// loading data from url
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data, let string = String(data: data, encoding: .utf8) {
// return result
promise.success(string)
} else {
// handle error
promise.error(ExampleError.cantLoadData)
}
}
task.resume()
// return the future
return promise.future
}
loadData().onSuccess { data in
DispatchQueue.main.async {
self.label.text = data
}
}.onError { error in
print(error)
// handle error
}
Composition
map
Returns the new Future with the result you return to closure or with error if the first Future contains error.
let future = Future<Int>(value: 100)
future.onSuccess { value in
// value == 100
}
let mapFuture = future.map { value -> String in
return "\(value) now it's string"
}
mapFuture.onComplete { result in
switch result {
case .value(let value):
print(value) // "100 now it's string""
case .error(let error):
// handle error
}
}
flatMap
Returns the new Future with the Future you return to closure or with error if the first Future contains error.
let future = Future<Int>(value: 1)
let flatMapFuture = future.flatMap { value -> Future<String> in
return Future<String>(value: "\(value * 100)%")
}
flatMapFuture.onSuccess { value in
print(value) // "100%"
}
filter
Returns the Future if value satisfies the filtering else returns error.
let future = Future<Int>(value: 500)
future.filter { value -> Bool in
return value > 100
}.onComplete { result in
switch result {
case .value(let value):
print(value) // 100
case .error(let error):
print(error) // no error
}
}
future.filter { value -> Bool in
return value > 1000
}.onComplete { result in
switch result {
case .value(let value):
print(value) // no value
case .error(let error):
print(error) // FutureError.filterError
}
}
recover
If the Future contains or will contain error you can recover it with the new value.
let future = Future<Int>(error: someError)
future.recover { error -> Int in
return 100
}.onComplete { result in
switch result {
case .value(let value):
print(value) // 100
case .error(let error):
print(error) // no error
}
}
zip
Combines two values into a tuple.
let first = Future<Int>(value: 1)
let second = Future<Int>(value: 2)
first.zip(second).onSuccess { firstValue, secondValue in
print(firstValue) // 1
print(secondValue) // 2
}
andThen
Returns the new Future with the same value.
let future = Future<String>(value: "and")
future.andThen { value in
print(value) // "and"
}.andThen { value in
print(value.count) // 3
}
flatten
If the value of the Future is the another Future you can flatten it.
let future = Future<Future<String>>(value: Future<String>(value: "value"))
future.onSuccess { value in
print(value) // Future<String>(value: "value")
}
future.flatten().onSuccess { value in
print(value) // "value"
}
Errors handling
map
, flatMap
, filter
and recover
can catch errors and return the Future with this error, so you don't need to handle it with do/catch
.
let future = Future<String>(value: "")
let errorToThrow = NSError(domain: "", code: -1, userInfo: nil)
future.map { value -> String in
throw errorToThrow
}.flatMap { value -> Future<String> in
throw errorToThrow
}.filter { value -> Bool in
throw errorToThrow
}.recover { error -> String in
throw errorToThrow
}
Sequences
EasyFutures provides some functions to help you work with the sequences of Futures.
fold
You can convert a list of the values into a single value. Fold returns the Future with this value. Fold takes default value and then you perform action with default value and every value from the list. Can catch errors.
let futures = [Future<Int>(value: 1), Future<Int>(value: 2), Future<Int>(value: 3)]
futures.fold(0) { defaultValue, currentValue -> Int in
return defaultValue + currentValue
}.onSuccess { value in
print(value) // 6
}
traverse
Traverse can work with any sequence. Takes closure where you transform the value into the Future. Returns the Future which contains array of the values from the Futures returned by the closure.
[1, 2, 3].traverse { number -> Future<String> in
return Future<String>(value: "\(number * 100)")
}.onSuccess { value in
print(value) // ["100", "200", "300"]
}
sequence
Transforms a list of the Futures into the single Future with an array of values.
let futures = [Future<Int>(value: 1), Future<Int>(value: 2), Future<Int>(value: 3)] // [Future<Int>, Future<Int>, Future<Int>]
let sequence = futures.sequence() // Future<[Int]>
sequence.onSuccess { numbers in
print(numbers) // [1, 2, 3]
}
License
EasyFutures is under MIT license. See the LICENSE file for more info.