Skip to content

PeakOperation is a Swift microframework providing enhancement and conveniences to Operation.

License

Notifications You must be signed in to change notification settings

3Squared/PeakOperation

Repository files navigation

Peak Operation

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.

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.

About

PeakOperation is a Swift microframework providing enhancement and conveniences to Operation.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages