OperationPlus is a set of NSOperation subclasses and extensions on NSOperation/NSOperationQueue. Its goal is to fill in the API's missing pieces. You don't need to learn anything new to use it.
NSOperation has been around for a long time, and there are now two potential first-party alternatives, Combine and Swift concurrency. OperationPlus includes some facilities to help Combine and NSOperation interoperate conveniently.
Swift Package Manager:
dependencies: [ .package(url: "https://github.com/ChimeHQ/OperationPlus.git") ]BaseOperation: provides core functionality for easierNSOperationsubclassingAsyncOperation: convenience wrapper aroundBaseOperationfor async supportAsyncBlockOperation: convenience class for inline async support(Async)ProducerOperation: produces an output(Async)ConsumerOperation: accepts an input from aProducerOperation(Async)ConsumerProducerOperation: accepts an input from aProducerOperationand also produces an output
BaseOperation
This is a simple NSOperation subclass built for easier extensibility. It features:
- Thread-safety
- Timeout support
- Easier cancellation handling
- Stricter state checking
- Built-in asynchrous support
- Straight-foward customization
let a = BaseOperation(timeout: 5.0) // NSOperation will happily allow you do this even // if `a` has finished. `BaseOperation` will not. a.addDependency(another) // ... public override func main() { // This will return true if your operation is cancelled, timed out, // or prematurely finished. ProducerOperation subclass state will be // handled correctly as well. if self.checkForCancellation() { return } } // ...AsyncOperation
A BaseOperation subclass that can be used for your asynchronous operations. These are any operations that need to extend their lifetime past the main method.
import Foundation import OperationPlus class MyAsyncOperation: AsyncOperation { public override func main() { DispatchQueue.global().async { if self.checkForCancellation() { return } // do stuff self.finish() } } }There's also nothing special about this class at all -- it's there just for convenience. If you want, you can just subclass BaseOperation directly and override one method.
import Foundation import OperationPlus class MyAsyncOperation: BaseOperation { override open var isAsynchronous: Bool { return true } }ProducerOperation
A BaseOperation subclass that yields a value. Includes a completion handler to access the value.
import Foundation import OperationPlus class MyValueOperation: ProducerOperation<Int> { public override func main() { // do your computation self.finish(with: 42) } } // ... let op = MyValueOperation() op.resultCompletionBlock = { (value) in // use value here }AsyncProducerOperation
A variant of ProducerOperation that may produce a value after the main method has completed executing.
import Foundation import OperationPlus class MyAsyncOperation: AsyncProducerOperation<Int> { public override func main() { DispatchQueue.global().async { if self.checkForCancellation() { return } // do stuff self.finish(with: 42) } } }ConsumerOperation and AsyncConsumerOperation
A BaseOperation sublass that accepts the input of a ProducerOperation.
import Foundation import OperationPlus class MyConsumerOperation: ConsumerOperation<Int> { override func main() { guard let value = producerValue else { // handle failure in some way } } override func main(with value: Int) { // make use of value here, or automatically // fail if it wasn't successfully produced } } let op = MyConsumerOperation(producerOp: myIntProducerOperation)AsyncBlockOperation
A play on NSBlockOperation, but makes it possible to support asynchronous completion without making an Operation subclass. Great for quick, inline work.
let op = AsyncBlockOperation { (completionBlock) in DispatchQueue.global().async { // do some async work here, just be certain to call // the completionBlock when done completionBlock() } }Queue creation conveniences:
let a = OperationQueue(name: "myqueue") let b = OperationQueue(name: "myqueue", maxConcurrentOperations: 1) let c = OperationQueue.serialQueue() let d = OperationQueue.serialQueue(named: "myqueue")Enforcing runtime constraints on queue execution:
OperationQueue.preconditionMain() OperationQueue.preconditionNotMain()Consise dependencies:
queue.addOperation(op, dependency: opA) queue.addOperation(op, dependencies: [opA, opB]) queue.addOperation(op, dependencies: Set([opA, opB])) op.addDependencies([opA, opB]) op.addDependencies(Set([opA, opB]))Queueing work when a queue's current operations are complete:
queue.currentOperationsFinished { print("all pending ops done") }Convenient inline functions:
queue.addAsyncOperation { (completionHandler) in DispatchQueue.global().async { // do some async work completionHandler() } }Async integration:
queue.addOperation { await asyncFunction1() await asyncFunction2() } let value = try await queue.addResultOperation { try await asyncValue() }Delays:
queue.addOperation(op, afterDelay: 5.0) queue.addOperation(afterDelay: 5.0) { // work }PublisherOperation
This ProducerOperation subclass takes a publisher. When executed, it creates a subscription and outputs the results.
op.publisher() // AnyPublisher<Void, Never> producerOp.outputPublisher() // AnyPublisher<Output, Never>publisher.operation() // PublisherOperation publisher.execute(on: queue) // subscribes and executes chain on queue and returns a publisher for resultOperationTestingPlus is an optional micro-framework to help make your XCTest-based tests a little nicer. When using Carthage, it is built as a static framework to help ease integration with your testing targets.
FulfillExpectationOperation
A simple NSOperation that will fulfill an XCTestExpectation when complete. Super-useful when used with dependencies on your other operations.
NeverFinishingOperation
A great way to test out your Operations' timeout behaviors.
OperationExpectation
An XCTestExpectation sublass to make testing async operations a little more XCTest-like.
let op = NeverFinishingOperation() let expectation = OperationExpectation(operation: op) expectation.isInverted = true wait(for: [expectation], timeout: 1.0)We'd love to hear from you! Get in touch via an issue or pull request.
Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.