ClosureChain
ClosureChain
simplifies sequential async completion methods for Swift. It provides a familiar try-catch pattern for sequential async methods.
Installation
Swift Package Manager
Add the ClosureChain
package to the dependencies within your application's Package.swift
file. Substitute "x.y.z" with the latest ClosureChain
release.
.package(url: "https://github.com/dannys42/ClosureChain.git", from: "x.y.z")
Add ClosureChain
to your target's dependencies:
.target(name: "example", dependencies: ["ClosureChain"]),
Cocoapods
Add ClosureChain
to your Podfile:
pod `ClosureChain`
Usage
Typically in Swift, network or other async methods make use of completion handlers to streamline work. A typical method signature looks like this:
func someAsyncMethod(_ completion: (Data?, Error?)->Void) {
}
However, this can become difficult to manage when you need to perform a number of async functions, each relying on success data from the previous call.
Normally this requires nesting the async methods or the use of a state machine. Both of which can be difficult to reason about.
Closure Chains simplify this by allowing the developer to treat each async call as linkage of throwable closures. (i.e. links in a chain), with a single catch
closure to manage any errors.
Simple Example
let chain = ClosureChain()
chain.try { link in
someAsyncMethod() { data, error in
if let error = error {
link.throw(error) // use `link.throw()` since completion block is not throwable
}
guard let data = data else {
link.throw(Failure.missingDdata) // use `link.throw()` since completion block is not throwable
return
}
// do something with `data`
link.success() // required
}
}
chain.catch { error in
// error handler
}
chain.start() // required to start executing links
Note the familiar try-catch
pattern. However try
is perfomed on the chain chain
, and the throw
is performed on the link
. As a convenience, you can simply use the Swift throw
command directly within a try-block.
There are two additional required functions:
link.success()
is required to letClosureChain
know when an async task is completechain.start()
is required to kick off execution of the chain. No links will be executed until the.start()
command is initiated.
Passing data
The above is not very useful when we only have one async operation. But what if we have several async operations that we wish to perform. For example imagine we are attempt to perform this sequence of tasks:
- Get raw image data from network
- Convert raw data to a UIImage object. Perhaps we have a long running async task here that will perform decryption, digital signature verification, and JSON deserialization
- Do more background processing on UIImage
- Notify the user we're done
For simplicity, we'll assume all our async methods are using the Result
protocol.
This is how this might look with ClosureChain
:
function closureChainExample() {
let chain = ClosureChain()
chain.try { link in
getDataAsync() { result: Result<Data,Error> in // Result type is provided solely for context in this example
switch result {
case .failure(let error):
link.throw(error) // use link.throw() since completion handler is not throwable
case .success(let data):
link.success(data) // Pass `data` to the next link
}
}
}
chain.try { data: Data, link in // `data` type must match prior link.success() (this check is performed at run-time)
convertToUIImage(data) { result: Result<UIImage,Error> in // Result type is provided solely for context in this example
switch result {
case .failure(let error):
link.throw(error) // use link.throw() since completion handler is not throwable
case .success(let uiimage):
link.success(uiimage) // Pass `uiimage` to the next link
}
}
}
chain.try { image: UIImage, link in // `image` type must match prior link.success()
processImage(image) { error: Error? in // Error type is provided solely for context in this example
do {
if let error = error {
throw(error) // can use do-catch to allow `throws` to pass to `link.throw()`
}
link.success() // Go to next link with no passed data
} catch {
link.throw(error)
}
}
}
chain.try { link in // It is safe to ignore the passed parameter from the last `link.success()`
// Notify the user we're done
link.success() // Required even though this is the last link
}
chain.catch { error in
// error handler
}
chain.start() // Required to start executing links
}
Notes:
chain
can be safely allowed to fall out-of-scope.chain
and the associated closures will not be released from memory untillink.success()
in the last link is called.- ClosureChains make no use of DispatchQueue or OperationQueue. Therefore there is no guarantee that any link is executing on any specific queue/thread.
Results can be Even Better
If your async methods have completion handlers that take a single Result parameter, as in the above example, you can further reduce your code:
function closureChainExample() {
let chain = ClosureChain()
chain.try { link in
getDataAsync() { result: Result<Data,Error> in // Result type is provided solely for context in this example
link.return(result) // calls link.throw() or link.success() appropriately
}
}
chain.try { data: Data, link in // `data` type must match prior link.success() (this check is performed at run-time)
convertToUIImage(data) { result: Result<UIImage,Error> in // Result type is provided solely for context in this example
link.return(result) // calls link.throw() or link.success() appropriately
}
}
chain.try { image: UIImage, link in // `image` type must match prior link.success()
processImage(image) { result: Result<UIImage,Error> in // Result type is provided solely for context in this example
link.return(result)
}
}
chain.try { link in // It is safe to ignore the passed parameter from the last `link.success()`
// Notify the user we're done
link.success() // Required even though this is the last link
}
chain.catch { error in
// error handler
}
chain.start() // Required to start executing links
}
API Documentation
For more information visit our API reference.
License
This library is licensed under Apache 2.0. The full license text is available in LICENSE.