- AOPeration
- AOperationQueue
- AOperationCondition
- AOperationObserver
- AOperationError
- AOperationDebugger
This is the most basic type of AOperatons. AOperation is a subclass of Operation that added some features like conditions, observers, declarative Api, ... to it. Because the AOperation class is an abstract class, you do not use it directly but instead subclass or use one of the pre-defined subclasses (DelayOperation or AOperationBlock) to perform the actual task. Despite being abstract, the base implementation of Operation does include significant logic to coordinate the safe execution of your task. The presence of this built-in logic allows you to focus on the actual implementation of your task, rather than on the glue code needed to ensure it works correctly with other system objects.
For subclassing you should not subclass AOperation directly. Instead use one of its two subclasses VoidOperation or ResultableOperation.
Use VoidOperation
if you don't expect any result of your task and you just want it be done.
For using VoidOperation
as subclass, create a class that inherited from VoidOperation
and override execute()
method. Do your tasks inside this method.
You should remember to call finish()
, to notify operation your task is done and finish operation.
If your task published an error, pass an AOperationError type error to operation by using finish(with: error)
.
class MyVoidOperation: VoidOperation {
//1 You should override execute method
override func execute() {
//2 do your task
//...
if taskIsDoneSuccessful {
//3 If it succeed finishing operation
self.finish()
}
else
//4 Or if it failed, finish operation with an error
self.finish(with: error)
}
}
}
You can observe finishing of your operation as below:
let operation = MyVoidOperation()
operation.didFinish { result in
if let error = result.error {
// Handle error
}
else {
//do your tasks
}
}
This method called after operation moves to finished state.
If you want to do something before operation moves to finished state use willFinish
state.
operation.willFinish { (result, finish) in
finish()
}
Note that you should call finish()
at the end of closure to move operation to finished state othewise operation stays unfinished and doesn't exits from queue.
Use ResultableOperation<Output>
if you expect some resutl from your operation. The Output
is type of output you expect from your task when its done.
For using ResultableOperation
as subclass, create a class that inherited from ResultableOperation<Output>
and override execute
method. Do your tasks inside this method.
You should remember to call finish(with:)
, with the received result from your task to notify operation your task is done and finishing operation.
The finish method accepts a Result
parameter. Give it a .success(value)
if your task succeed or a .failure(error)
if your task failed. The error should be type of AOperationError.
class MyOperation: ResultableOperation<Data> {
//1 You should override execute method
override func execute() {
//2 do your task
someTask { taskOutput, error
if let taskError = error {
//3 Finish operation with error if task produced an error
let operationError = AOperationError(taskError)
self.finish(with: .failure(operationError))
}
else {
self.finish(with: .success(taskOutput))
}
}
}
}
You can observe finishing of your operation as below:
let operation = MyOperation()
operation.didFinish { result in
switch result {
case .success(let data):
// do your task here
case .failure(let error):
// handle error here
}
}
This method called after operation moves to finished state.
If you want to do something before operation moves to finished state use willFinish
state.
operation.willFinish { (result, finish) in
finish()
}
Note that you should call finish()
at the end of closure to move operation to finished state othewise operation stays unfinished and doesn't exits from queue.
A Group Operation is an Operation that takes several operations as input. The Group Operation finishes only if all the given operations finish their execution.
Note: Group operation doesn't guarantees which operation start first but it guarantees to finish only if all operation got finish.
let groupOperation = GroupOperation(operationA, operationB, operationC)
groupOperation.didFinish { (errors) in
//...
here is where all of the operationA, operationB and operationC are finished
}
Also you can subclass GroupOperation
to have a reusable operation.
class MyOperatoin: GroupOperation {
init() {
let operationA = OperationA()
let operationB = OperationB()
let operationC = OperationC()
super.init([operationA, operationB, operationC])
}
}
Ordered Group Operation is like a group operation whith the difference that the given operations execute in order. It means the first operation starts first and finishes first and last operation starts last and finishes last. OrderedGroupOperation finishes only if all the given operations get finish.
let groupOperation = OrderedGroupOperation(operationA, operationB, operationC)
groupOperation.didFinish { (errors) in
//...
here is where all of the operationA, operationB and operationC are finished
}
Also you can subclass OrderedGroupOperation
to have a reusable operation.
class MyOperatoin: OrderedGroupOperation {
init() {
let operationA = OperationA()
let operationB = OperationB()
let operationC = OperationC()
super.init([operationA, operationB, operationC])
}
}
In above examples operations execute in order, means operationA starts first and finishes first and operationC starts last and finishes last.
An Operation that wraps another operation. Consider an operation that you give it a URLRequest to do a task:
let url = URL(string: "A url string")!
var request = URLRequest(url: url)
request.allHTTPHeaderFields =
["Authorization" : "An authorization code"]
let operation = URLSessionTaskOperation.data(for: request)
Using WrapperOperation
you can wrap all of this lines of code and make a reusable operation:
let operation = WrapperOperation<Void, (response: URLResponse, data: Data)> { _ -> ResultableOperation<(response: URLResponse, data: Data)> in
let url = URL(string: "A url string")!
var request = URLRequest(url: url)
request.allHTTPHeaderFields =
["Authorization" : "An authorization code"]
return URLSessionTaskOperation.data(for: request)
}
Another way to create a WrapperOperation
is subclassing it:
class AuthorizedOperation: WrapperOperation<Void, (response: URLResponse, data: Data)> {
init() {
super.init { _ -> ResultableOperation<(response: URLResponse, data: Data)> in
let url = URL(string: "A url string")!
var request = URLRequest(url: url)
request.allHTTPHeaderFields =
["Authorization" : "An authorization code"]
return URLSessionTaskOperation.data(for: request)
}
}
}
A protocol with methods for react to the changes of operation's lifecycle. Methods of this protocol notify when an operation starts to executtion and when an operation finishes its execution. To use this delegate, set the delegate
property of operation equal to an object that conformed This Delegate.
class OperationDelegateManager: AOperationDelegate {
func operationDidStart(_ operation: AOperation) {
}
func operationDidFinish(_ operation: AOperation, with errors: [AOperationError]) {
}
}
let operation = MyOperation()
operation.delegate = OperationDelegateManager()
Note that all them methods of this delegate are optional.
A subclass of OperationQueue that implements a large
number of "extra features" related to the AOperation
class like:
- Notifying a delegate of all operation completion
- Extracting generated dependencies from operation conditions
- Setting up dependencies to enforce mutual exclusivity
To use an AOperationQueue
define an instance with type of AOperationQueue:
let queue = AOperationQueue()
Then add an Operation
to it.
let operation = SampleOperation()
queue.addOperation(operation)
Another way of adding an operation to AoperationQueue is like below:
SampleOperation()
.add(to: queue)
A global instance of AOperationQueue
that you can use it if you need.
SampleOperation()
.add(to: .shared)
A protocol for defining conditions that must be satisfied for an operation to begin execution.
you create a condition by adopting AOperationCondition
to a struct. By adding the condition to an operation. the satisfaction of operation will be check before operation start to execute.
AOperationCondition
protocol has two required element needs to be conform.dependentOperation
and isMutuallyExclusive
.
dependentOperation
is the operation that doing tasks need to candition be satisfied. This operation added to the main operation as dependency and will be starts before main operation. By default if dependency operation fails the condition fails and the main operation will not execute.
By setting isMutuallyExclusive
true, it prevents other condtions with the same type and their dependency operations execute and be check concurrently.
There are two other methods that have default implementions.
First func dependencyForOperation(_ operation: AOperation) -> AOperation?
that returns dependentOperation
by default, but by overriding this method you can handle it in your costum way.
Second func evaluateForOperation(_ operation: AOperation, completion: @escaping (OperationConditionResult) -> Void)
that checks satisfaction of condition. This method by default checks if dependency operation has any published errors fails the given completion of method otherwise satisfies it. By overriding you can customize stisfaction of condition manually.
struct UserAccessConditoin: AOperationCondition {
static var isMutuallyExclusive: Bool = false
let dependentOperation: AOperation? = nil
func evaluateForOperation(_ operation: AOperation, completion: @escaping (OperationConditionResult) -> Void) {
if User.current.hasAccess {
completion(.success)
}
else {
let error = AOperationError(AccessError.accessDenied)
completion(.failure(error))
}
}
}
in above example, we check user access manually.
Another way of creating a UserAccessConditoin
is like below:
struct UserAccessConditoin: AOperationCondition {
static var isMutuallyExclusive: Bool = true
let dependentOperation: AOperation? = RequestUserAccessOperation()
}
In above exampleRequestUserAccessOperation
will be added as dependency to main operation, and executes first. RequestUserAccessOperation
requests authorization for user to edit contents. If the operation fails the condition be fail. So the operation that this conditon added to it, will not be execute.
By setting isMutuallyExclusive
true, we make sure never two RequestUserAccessOperation
executed concurrently.
Adding a condition to an operation is like below:
let operation = LikeCommentOperation(for: comment)
operation.conditions(UserAccessConditoin())
operation.didFinish { result in
switch result {
case .success:
//update UI
case .failure(let error):
// You should handle received error
//The failed error may be cause of UserAccessConditoin or LikeCommentOperation
}
}
The protocol that types may conform if they wish to be notified of significant operation lifecycle events.
This protocol notifies on operation starting to execution and finish of, also on produce a new operation.
A concerate example of AOperationObserver is TimeoutObserver
and BlockObserver
.
AOperation didStart(_:)
and didFinish(_:)
methods are work based on BlockObserver
.
This is type of error used in AOperation. If your task failed and faced with error you finish the operation by passing an AOperationError created with the given error:
let error = AOperationError(receivedError)
self.finish(error)
Each AOperationError
contains some information:
- state: Announces the state error published that wether is on condition satisfy checking (.condition) or operation execution (.execution)
- publisher: Name of error publisher. By default is the type of condition or operation, published error.
- publishedError: the published error by the failur task that passed to AOperationError.
Note that the localizedDescription
property on AOperationError directly returns localizedDescription
of publishedError.
There are two predefined Error which you may encounter them.
An error publishes when operation being cancel.
An error you can publish when receivedValue
of a ReceiverOperation is nil.
A tool for tracking operations lifecycle. You can simply debug and track any AOperation by using below code every where before running your operation:
AOperation.Debugger.printOperationsState = true
This line of code prints states of executed operation in debugger like below:
AOperation MyOperation added to queue
AOperation MyOperation executed
AOperation MyOperation cancelled
AOperation MyOperation finished
This way you can track any AOperation from start to finish. This is useful when you see one of your Operations remained unfinished and you wonder which one is.
Also sometimes AOperationDebugger gives you hints about why your operation not executed. For example you see this line if you forgot override execute()
method:
AOperation MyOperation must override `execute()`.
Or you see this error if your operation conforms to UniqueOperation :
AOperation MyOperation ignored because of uniqueId (operation uniqueId)