Lightweight full-featured Promises, Async & Await Library in Swift
What's this?
Hydra is full-featured lightweight library which allows you to write better async code in Swift 3.x/4.x. It's partially based on JavaScript A+ specs and also implements modern construct like await
(as seen in Async/Await specification in ES8 (ECMAScript 2017) or C#) which allows you to write async code in sync manner.
Hydra supports all sexiest operators like always, validate, timeout, retry, all, any, pass, recover, map, zip, defer and retry.
Starts writing better async code with Hydra!
A more detailed look at how Hydra works can be found in ARCHITECTURE file or on Medium.
❤️ Your Support
Hi fellow developer!
You know, maintaing and developing tools consumes resources and time. While I enjoy making them your support is foundamental to allow me continue its development.
If you are using SwiftLocation or any other of my creations please consider the following options:
What's a Promise?
A Promise is a way to represent a value that will exists, or will fail with an error, at some point in the future. You can think about it as a Swift's Optional
: it may or may not be a value. A more detailed article which explain how Hydra was implemented can be found here.
Each Promise is strong-typed: this mean you create it with the value's type you are expecting for and you will be sure to receive it when Promise will be resolved (the exact term is fulfilled
).
A Promise is, in fact, a proxy object; due to the fact the system knows what success value look like, composing asynchronous operation is a trivial task; with Hydra you can:
- create a chain of dependent async operation with a single completion task and a single error handler.
- resolve many independent async operations simultaneously and get all values at the end
- retry or recover failed async operations
- write async code as you may write standard sync code
- resolve dependent async operations by passing the result of each value to the next operation, then get the final result
- avoid callbacks, pyramid of dooms and make your code cleaner!
Updating to >=0.9.7
Since 0.9.7 Hydra implements Cancellable Promises. In order to support this new feature we have slightly modified the Body
signature of the Promise
; in order to make your source code compatible you just need to add the third parameter along with resolve
,reject
: operation
.
operation
encapsulate the logic to support Invalidation Token
. It's just and object of type PromiseStatus
you can query to see if a Promise is marked to be cancelled from the outside.
If you are not interested in using it in your Promise declaration just mark it as _
.
To sum up your code:
needs to be:
Create a Promise
Creating a Promise is trivial; you need to specify the context
(a GCD Queue) in which your async operations will be executed in and add your own async code as body
of the Promise.
This is a simple async image downloader:
You need to remember only few things:
- a Promise is created with a type: this is the object's type you are expecting from it once fulfilled. In our case we are expecting an
UIImage
so our Promise isPromise<UIImage>
(if a promise fail returned error must be conform to Swift'sError
protocol) - your async code (defined into the Promise's
body
) must alert the promise about its completion; if you have the fulfill value you will callresolve(yourValue)
; if an error has occurred you can callreject(occurredError)
or throw it using Swift'sthrow occurredError
. - the
context
of a Promise define the Grand Central Dispatch's queue in which the async code will be executed in; you can use one of the defined queues (.background
,.userInitiated
etc. Here you can found a nice tutorial about this topic)
How to use a Promise
Using a Promise is even easier.
You can get the result of a promise by using then
function; it will be called automatically when your Promise fullfill with expected value.
So:
As you can see even then
may specify a context (by default - if not specified - is the main thread): this represent the GCD queue in which the code of the then's block will be executed (in our case we want to update an UI control so we will need to execute it in .main
thread).
But what happened if your Promise fail due to a network error or if the image is not decodable? catch
func allows you to handle Promise's errors (with multiple promises you may also have a single errors entry point and reduce the complexity).
Chaining Multiple Promises
Chaining Promises is the next step thought mastering Hydra. Suppose you have defined some Promises:
Each promise need to use the fulfilled value of the previous; plus an error in one of these should interrupt the entire chain.
Doing it with Hydra is pretty straightforward:
Easy uh? (Please note: in this example context is not specified so the default .main
is used instead).
Cancellable Promises
Cancellable Promises are a very sensitive task; by default Promises are not cancellable. Hydra allows you to cancel a promise from the outside by implementing the InvalidationToken
. InvalidationToken
is a concrete open class which is conform to the InvalidatableProtocol
protocol.
It must implement at least one Bool
property called isCancelled
.
When isCancelled
is set to true
it means someone outside the promise want to cancel the task.
It's your responsibility to check from inside the Promise
's body the status of this variable by asking to operation.isCancelled
.
If true
you can do all your best to cancel the operation; at the end of your operations just call cancel()
and stop the workflow.
Your promise must be also initialized using this token instance.
This is a concrete example with UITableViewCell
: working with table cells, often the result of a promise needs to be ignored. To do this, each cell can hold on to an InvalidationToken
. An InvalidationToken
is an execution context that can be invalidated. If the context is invalidated, then the block that is passed to it will be discarded and not executed.
To use this with table cells, the queue should be invalidated and reset on prepareForReuse()
.
Await & Async: async code in sync manner
Have you ever dream to write asynchronous code like its synchronous counterpart? Hydra was heavily inspired by Async/Await specification in ES8 (ECMAScript 2017) which provides a powerful way to write async doe in a sequential manner.
Using async
and await
is pretty simple.
NOTE: Since Hydra 2.0.6 the await function is available under Hydra.await() function in order to supress the Xcode 12.5+ warning (await will become a Swift standard function soon!)
For example the code above can be rewritten directly as:
Like magic! Your code will run in .background
thread and you will get the result of each call only when it will be fulfilled. Async code in sync sauce!
Important Note: await
is a blocking/synchronous function implemented using semaphore. Therefore, it should never be called in main thread; this is the reason we have used async
to encapsulate it. Doing it in main thread will also block the UI.
async
func can be used in two different options:
- it can create and return a promise (as you have seen above)
- it can be used to simply execute a block of code (as you will see below)
As we said we can also use async
with your own block (without using promises); async
accepts the context (a GCD queue) and optionally a start delay interval.
Below an example of the async function which will be executed without delay in background:
There is also an await operator:
- await with throw:
..
followed by a Promise instance: this operator must be prefixed bytry
and should usedo/catch
statement in order to handle rejection of the Promise. - await without throw:
..!
followed by a Promise instance: this operator does not throw exceptions; in case of promise's rejection result is nil instead.
Examples:
When you use these methods and you are doing asynchronous, be careful to do nothing in the main thread, otherwise you risk to enter in a deadlock situation.
The last example show how to use cancellable async
:
Await an zip
operator to resolve all promises
Await can be also used in conjuction with zip to resolve all promises from a list:
All Features
Because promises formalize how success and failure blocks look, it's possible to build behaviors on top of them.
Hydra supports:
always
: allows you to specify a block which will be always executed both forfulfill
andreject
of the Promisevalidate
: allows you to specify a predica block; if predicate returnfalse
the Promise fails.timeout
: add a timeout timer to the Promise; if it does not fulfill or reject after given interval it will be marked as rejected.all
: create a Promise that resolved when the list of passed Promises resolves (promises are resolved in parallel). Promise also reject as soon as a promise reject for any reason.any
: create a Promise that resolves as soon as one passed from list resolves. It also reject as soon as a promise reject for any reason.pass
: Perform an operation in the middle of a chain that does not affect the resolved value but may reject the chain.recover
: Allows recovery of a Promise by returning another Promise if it fails.map
: Transform items to Promises and resolve them (in paralle or in series)zip
: Create a Promise tuple of a two promisesdefer
: defer the execution of a Promise by a given time interval.cancel
: cancel is called when a promise is marked ascancelled
usingoperation.cancel()
always
always
func is very useful if you want to execute code when the promise fulfills — regardless of whether it succeeds or fails.
validate
validate
is a func that takes a predicate, and rejects the promise chain if that predicate fails.
timeout
timeout
allows you to attach a timeout timer to a Promise; if it does not resolve before elapsed interval it will be rejected with .timeoutError
.
all
all
is a static method that waits for all the promises you give it to fulfill, and once they have, it fulfills itself with the array of all fulfilled values (in order).
If one Promise fail the chain fail with the same error.
Execution of all promises is done in parallel.
If you add promise execution concurrency restriction to all
operator to avoid many usage of resource, concurrency
option is it.
any
any
easily handle race conditions: as soon as one Promise of the input list resolves the handler is called and will never be called again.
pass
pass
is useful for performing an operation in the middle of a promise chain without changing the type of the Promise.
You may also reject the entire chain.
You can also return a Promise from the tap handler and the chain will wait for that promise to resolve (see the second then
in the example below).
recover
recover
allows you to recover a failed Promise by returning another.
map
Map is used to transform a list of items into promises and resolve them in parallel or serially.
zip
zip
allows you to join different promises (2,3 or 4) and return a tuple with the result of them. Promises are resolved in parallel.
defer
As name said, defer
delays the execution of a Promise chain by some number of seconds from current time.
retry
retry
operator allows you to execute source chained promise if it ends with a rejection.
If reached the attempts the promise still rejected chained promise is also rejected along with the same source error.
Retry also support delay
parameter which specify the number of seconds to wait before a new attempt (2.0.4+).
Conditional retry allows you to control retryable if it ends with a rejection.
cancel
cancel
is called when a promise is marked as cancelled
from the Promise's body by calling the operation.cancel()
function. See the Cancellable Promises for more info.
Chaining Promises with different Value
types
Sometimes you may need to chain (using one of the available operators, like all
or any
) promises which returns different kind of values. Due to the nature of Promise you are not able to create an array of promises with different result types.
However thanks to void
property you are able to transform promise instances to generic void
result type.
So, for example, you can execute the following Promises
and return final values directly from the Promise's result
property.
Installation
You can install Hydra using CocoaPods, Carthage and Swift package manager
- Swift 3.x: Latest compatible is 1.0.2
pod 'HydraAsync', ~> '1.0.2'
- Swift 4.x: 1.2.1 or later
pod 'HydraAsync'
CocoaPods
use_frameworks!
pod 'HydraAsync'
Carthage
github 'malcommac/Hydra'
Swift Package Manager
Add Hydra as dependency in your Package.swift
import PackageDescription
let package = Package(name: "YourPackage",
dependencies: [
.Package(url: "https://github.com/malcommac/Hydra.git", majorVersion: 0),
]
)
Consider ❤️ support the development of this library!
Requirements
Current version is compatible with:
- Swift 5.x
- iOS 9.0 or later
- tvOS 9.0 or later
- macOS 10.10 or later
- watchOS 2.0 or later
- Linux compatible environments
Contributing
- If you need help or you'd like to ask a general question, open an issue.
- If you found a bug, open an issue.
- If you have a feature request, open an issue.
- If you want to contribute, submit a pull request.
Copyright & Acknowledgements
SwiftLocation is currently owned and maintained by Daniele Margutti.
You can follow me on Twitter @danielemargutti.
My web site is https://www.danielemargutti.com
This software is licensed under [MIT License]
Follow me on:
<h2 id="github">GitHub</h2>
<p><a class="github-view" href="https://github.com/malcommac/Hydra" target="_blank" rel="nofollow noopener">https://github.com/malcommac/Hydra</a></p>