PeakOperation is a Swift microframework providing enhancement and conveniences to Operation
. It is part of the Peak Framework.
Concurrent Operations
ConcurrentOperation
is an abstract Operation
subclass that can perform work asynchronously. You override execute()
to perform your work, and when it is completed call finish()
to complete the operation.
class MyOperation: ConcurrentOperation {
override func execute() {
print("Hello World!")
finish()
}
}
let queue = OperationQueue()
let operation = MyOperation()
operation.enqueue(on: queue)
This means that you can perform asynchronous work inside execute()
, such as performing a URLSession
request.
Chaining operations
Operation
provides the ability to add dependencies between operations. PeakOperation builds upon this functionality and wraps it in an easy-to-use API.
let firstOperation = ...
let secondOperation = ...
firstOperation
.then(do: secondOperation)
.enqueue()
In the example above, secondOperation
will run once firstOperation
finishes.
You can also call enqueueWithProgress()
on a chain of operations or overallProgress()
on a single operation to track their progress.
let progress: Progress = firstOperation
.then(do: secondOperation)
.enqueueWithProgress()
// or
let progress: Progress = secondOperation.overallProgress()
Passing Results
Adding dependencies between operations is useful, but passing results between operations is where PeakOperation really shines. PeakOperation includes two protocols your operation can conform to: ProducesResult
and ConsumesResult
.
Let's say we have three operations. The first produces a Result<Int, Error>
.
class IntOperation: ConcurrentOperation, ProducesResult {
var output: Result<Int, Error>
override func execute() {
output = Result { 1 }
finish()
}
}
The second operation consumes a Result<Int, Error>
and produces a Result<String, Error>
. It unpacks its input, adds 1, converts it to a string, then sets it's output.
class AddOneOperation: ConcurrentOperation, ConsumesResult, ProducesResult {
var input: Result<Int, Error>
var output: Result<String, Error>
override func execute() {
output = Result { "\(try input.get() + 1)" }
finish()
}
}
The final operation consumes a Result<String, Error>
. It unpacks it and prints it to the console:
class PrintOperation: ConcurrentOperation, ConsumesResult {
var input: Result<String, Error>
override func execute() {
do {
print("Hello \(try input.get())!")
} catch { }
finish()
}
}
Using passesResult(to:)
, these three operations can be chained together!
IntOperation()
.passesResult(to: AddOneOperation())
.passesResult(to: PrintOperation())
.enqueue()
// Hello 2!
As long as the input type matches the output type, you can pass results between any operations conforming to the protocols.
If any of the operations fail and its result is .failure
, then the result will still be passed into the next operation. It's up to you to unwrap the result and deal with the error appropriately, perhaps by rethrowing the error.
class RethrowingOperation: ConcurrentOperation, ConsumesResult, ProducesResult {
var input: Result<String, Error>
var output: Result<String, Error>
override func execute() {
do {
let _ = try input.get()
output = ...
} catch {
output = Result { throw error }
}
finish()
}
}
That way, any of the operations can fail and you can still retrieve the error at the end.
let failingOperation = ...
let successfulOperation = ...
let anotherSuccessfulOperation = ...
failingOperation
.passesResult(to: successfulOperation)
.passesResult(to: anotherSuccessfulOperation)
.enqueue()
anotherSuccessfulOperation.addResultBlock { result in
// result would contain the error from failingOperation
// even though the other operations still ran
}
Grouping
GroupChainOperation
takes an operation and its dependants and executes them on an internal queue. The result of the operations is retained and it is inspected in order that this operation can produce a result of type Result<Void, Error>
- the value is lost, but the .success
/.failure
is kept. This allows you to chain together groups with otherwise incompatible input/outputs.
// would otherwise produce String, now produces Void
let group1 = intOperation
.passesResult(to: stringOperation)
.group()
// Would otherwise accept Bool, now consumes Void
let group2 = boolOperation
.passesResult(to: anyOperation)
.group()
group1
.passesResult(to: group2)
.enqueue()
Retrying
Sometimes an operation might fail. Perhaps you are dealing with a flaky web service or connection. For this, you can subclass RetryingOperation
. This is an operation which ProducesResult
, and if the result is .failure
, it will try again using a given retryStrategy
closure.
class MyRetryingOperation: RetryingOperation<AnyObject> {
override func execute() {
output = Result { throw error }
finish()
}
}
let operation = MyRetryingOperation()
operation.retryStrategy = { failureCount in
return failureCount < 3
}
You can provide your own block as a retryStrategy
. Here, the operation will be run 3 times before it finally fails.
There are 2 provided StrategyBlocks
:
RetryStrategy.none
RetryStrategy.repeat(times: Int)
Examples
Please see the included tests for further examples. Also check out PeakNetwork which uses PeakOperation extensively.
Getting Started
Installing
- Using Cocoapods, add
pod 'PeakOperation'
to your Podfile. import PeakOperation
where necessary.
Contributing
Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.
Versioning
We use SemVer for versioning.
License
This project is licensed under the MIT License - see the LICENSE.md file for details
Acknowledgments
Peak Framework
The Peak Framework is a collection of open-source microframeworks created by the team at 3Squared, named for the Peak District. It is made up of:
Name | Description |
---|---|
PeakCoreData | Provides enhances and conveniences to Core Data . |
PeakNetwork | A networking framework built on top of Session using PeakOperation, leveraging the power of Codable . |