Utility for asynchronous operations, written in Swift.
With HNTask
, you can organize asynchronous operations in the pattern like a JavaScript Promise. The core algorithm is inspired by BFTask
in Bolts-iOS and the core syntax came from JavaScript Promise.
In this example, countUserAsync()
and makeTotalUserStringAsync()
are functions which require some asynchronous operation to get a result. Each of these functions returns an HNTask
object.
When the asynchronous operation is done in success, the next then-block is called. If an error occurs in the operation, then-block is skipped and the next catch-block is called.
userList.countUsersAsync().then { (count: Int) in
if count <= 0 {
return HNTask.reject(NSError(domain: "MyDomain",
code: 1,
userInfo: nil))
} else {
return makeTotalUserStringAsync(count)
}
}.then { (message: String) in
showMessage(message)
return nil
}.catch { error in
let err = error as NSError
showMessage(err.description)
return nil
}
Use HNTask(callback:)
to create a new task. It returns an unresolved (uncompleted) task object which should be resolved or rejected when the operation is done. The passed callback block is called immediately to start asynchronous operation. It takes two function parameters, resolve
and reject
. Call one of these function to resolve or reject the task.
For convenience, you can also create a new task by HNTask.resove()
or HNTask.reject()
. These functions return a resolved or rejected task. Use these functions if you know the result of the task before creating it.
For more information about rejecting, see Error Handling.
let unresolvedTask = HNTask { (resolve, reject) in
// do some asynchronous operation
SomeAPI.post(url,
success: { result in
resolve(result)
},
failure: { error in
reject(error)
})
}
let resolvedTask = HNTask.resolve(100)
let rejectedTask = HNTask.reject(MyError(code: 100))
An HNTask
object has a method then()
. It returns a new HNTask
object. You can chain then-blocks by calling then()
of the returned HNTask
.
The then-block, closure parameter of then()
, is executed after the task is resolved. The block takes one parameter whose value is the result of the task, which was passed to resolve
function or was a return value in previous then-block. If you specify the type of the closure parameter, as shown in the first and the second then-blocks in example below, the type of the result value is checked. When types are mismatch, the then-block is not executed and the task is rejected with an HNTaskTypeError
value. You cannot specify an optional (such as FooType?
) in type.
You must return a result value in the block. If you have no result, return nil
. When you return an HNTask
object in then-block, it is executed prior to next block. In the following example, the last then-block, in which a value is printed out, is executed after the task returned by eatAsync()
is executed. In fact, it is the time resolve("I ate \(food)")
run.
func eatAsync(food: String) -> HNTask {
let task = HNTask { (resolve, reject) in
// suppose callItAfter runs the block 300 milliseconds later
callItAfter(300) {
resolve("I ate \(food)")
}
}
return task
}
HNTask.resolve(3).then { (number: Int) in
return "\(number) apples" // number == 3
}.then { (string: String) in
return eatAsync(string) // string == "3 apples"
}.then { value in
println(value) // value == "I ate 3 apples."
return nil
}
When an asynchronous operation fails, you can make an error by calling reject
function which is passed as parameter in callback block of the initializer (see Creating a New Task). If an error has occured in then-block, you can reject the task chain by returning rejected HNTask
object.
Both of reject
function or HNTask.reject()
take one error value. Any object can be an error value so you can pass such as NSError, String, Int, or your custom error object.
If a task was rejected, next then-blocks are not called but catch-block is called. You can handle errors in catch-block. The error object which was used in rejection is passed to catch-block as a pameter.
The method catch()
returns a new HNTask
like then()
and you can chain more then-block and/or catch-block.
class MyError {
let code: Int
init(code: Int) {
self.code = code
}
}
HNTask.resolve(-3).then { (number: Int) in
if number >= 0 {
return "\(number) apples"
} else {
return HNTask.reject(MyError(code: -3))
}
}.then { value in
// this block will not be executed
return nil
}.catch { error in
if let myError = error as? MyError {
println(myError.code)
}
return nil
}
The method finally()
returns a new HNTask
like then()
but the returned task will be resolved or rejected with the same value of the previous task, in other words, it does not modify the final value. You can return another task in finally-block. In this case, the completion of the task returned by finally()
will be delayed until the task returned by finally-block is finished. If you don't have another task in finally-block, simply return nil. Unlike then()
, you cannot return other values because the finally-block cannot change the resolved value (or rejected value) of the task.
You can run tasks in series by simply chaining tasks. Here is an example of tasks in the for-in loop.
userList.countUsersAsync().then { (count: Int) in
var task = HNTask.resolve(nil)
for index in 0..count {
task = task.then { value in
return userList.getUserNameAsync(index)
}.then { value in
if let name = value as? String {
addNameToList(name)
}
return nil
}
}
return nil
}
By using HNTask.all()
, you can wait until all tasks are resolved. As following example, HNTask.all()
returns an HNTask object and next then-block receives the array contains the resolved values in the same order as the original tasks.
If one of the tasks is rejected, the task returned by HNTask.all()
is rejected immediately. If you want to wait until all tasks are completed (resoved or even rejected), use HNTask.allSettled()
.
let tasks = [
userList.getUserNameAsync(1),
userList.getUserNameAsync(3),
userList.getUserNameAsync(5)
]
HNTask.all(tasks).then { value in
// after all task is resolved, this block is executed.
// the parameter value is an array contains the
// resolved values of each task in the same order.
let list = value as [Any?]
for v in list {
if let name = v as? String {
addNameToList(name)
}
}
return nil
}.catch { error in
// when one of the tasks rejected, this block is executed
// in this case, other tasks could be uncompleted yet
println(error)
return nil
}
HNTask.allSettled(tasks).then { value in
// after all task is resolved/rejected, this block is executed
// parameter value is an array contains resolved/rejected values
// of each task in the same order.
let list = value as [Any?]
for v in list {
if let error = v as? MyError {
println(error)
} else if let name = v as? String {
addNameToList(name)
}
}
return nil
}
By using HNTask.race()
, you can wait until one of the task is resolved. In this case, the next then-block receives the one result value of the resolved task.
func setTimeoutAsync(milliseconds: Int) -> HNTask {
return HNTask { (resolve, reject) in
callItAfter(milliseconds) {
resolve("(timeout)")
}
}
}
HNTask.race([
userList.getUserNameAsync(1),
setTimeoutAsync(1000)
]).then { value in
// if getUserNameAsync() takes more time than 1 second,
// the result will be "(timeout)"
if let name = value as? String {
addNameToList(name)
}
return nil
}
A subsequent task generated by then()
or catch()
is executed by "executor". An executor is the object which conforms to the HNExecutor
protocol. Both then()
and catch()
have the version which takes an executor at first parameter. If you don't specify an executor, i.e. you use the method which takes no executor, an instance of HNAsyncExecutor
class is used as the default executor. If you want to change the default executor, you can assign an executor to HNTask.DefaultTaskExecutor.sharedExecutor
.
There are three executor classes. HNAsyncExecutor
, HNDispatchQueueExecutor
and HNMainQueueExecutor
.
HNAsyncExecutor
executes a task in background asynchronously. To get an instance of HNAsyncExecutor
, use HNAsyncExecutor.sharedExecutor
instead of creating a new instance.
For convenience, HNAsyncExecutor
has method runAsync()
. You can use this method to create an asynchronous task easily.
func doSomethingAsync() -> HNTask {
return HNAsyncExecutor.sharedExecutor.runAsync() {
// do something asynchrounously
...
return theResultOfTask
}
}
HNDispatchQueueExecutor
executes a task on specified GCD queue. It also has runAsync()
and makes you possible to create an asynchrounous task which should be executed on specific queue.
HNMainQueueExecutor
executes a task on main queue. The task is executed on main thread. To get an instance of HNMainQueueExecutor
, use HNMainQueueExecutor.sharedExecutor
instead of creating a new instance.
By using this executor, you can force the task execute on main thread. For example, you may want to update the UI controls on main thread.
userList.getUserNameAsync(1).then(HNMainQueueExecutor.sharedExecutor) { (value: String) in
// this block is executed on main thread.
nameLabel.text = value
return nil
}
In addition to that, you can also create your own executor by adapting to the HNExecutor
protocol.