In Swift, using OperationQueue
for asynchronous code may seem like pure hell because, under the hood, Operations
are considered complete if the compilation of their synchronous code is completed.
In other words, compiling the example described below will output a broken execution order since, by the time the asynchronous code is executed, the Operation
itself will have already been completed.
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1
operationQueue.addOperation {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print("First async operation complete")
}
print("First sync operation complete")
}
operationQueue.addOperation {
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
print("Second async operation complete")
}
print("Second sync operation complete")
}
This code will print:
First sync operation complete
Second sync operation complete
First async operation complete
Second async operation complete
However, there is a way to circumvent these restrictions. To understand how to solve the problem, you need to understand how Operation
works under the hood.
The Operation
itself has four flags by which you can track the life cycle of the operation:
isReady
— indicates whether the Operation
can be performed at this time.
isExecuting
—indicates whether an Operation
is currently in progress.
isFinished
—indicates whether the Operation
is currently completed.
isCancelled
—indicates whether the Operation
was canceled.
In theory, the Operation
enters the isFinished
state before the Operation
itself is executed asynchronously, so we need to develop a technique by which we will be able to manipulate the life cycle of the Operation
.
This possibility can be solved by subclassing the Operation
and also by redefining the start
/ cancel
methods, as well as all the flags on which the operation's life cycle is built.
Here’s the code:
public class AsyncOperation: Operation {
// MARK: Open
override open var isAsynchronous: Bool {
true
}
override open var isReady: Bool {
super.isReady && self.state == .ready
}
override open var isExecuting: Bool {
self.state == .executing
}
override open var isFinished: Bool {
self.state == .finished
}
override open func start() {
if isCancelled {
state = .finished
return
}
main()
state = .executing
}
override open func cancel() {
super.cancel()
state = .finished
}
// MARK: Public
public enum State: String {
case ready
case executing
case finished
// MARK: Fileprivate
fileprivate var keyPath: String {
"is" + rawValue.capitalized
}
}
public var state = State.ready {
willSet {
willChangeValue(forKey: newValue.keyPath)
willChangeValue(forKey: state.keyPath)
}
didSet {
didChangeValue(forKey: oldValue.keyPath)
didChangeValue(forKey: state.keyPath)
}
}
}
The subclass we received from the Operation
is basic and allows us to forcefully complete it manually.
To work with completion blocks, you should create another subclass. However, this will not be a subclass of the Operation
, but of AsyncOperation
.
public typealias VoidClosure = () -> Void
public typealias Closure<T> = (T) -> Void
public class CompletionOperation: AsyncOperation {
// MARK: Lifecycle
public init(completeBlock: Closure<VoidClosure?>?) {
self.completeBlock = completeBlock
}
// MARK: Public
override public func main() {
DispatchQueue.main.async { [weak self] in
self?.completeBlock? {
DispatchQueue.main.async {
self?.state = .finished
}
}
}
}
// MARK: Private
private let completeBlock: Closure<VoidClosure?>?
}
This subclass will allow us to pass a closure to the Operation
, after which the Operation
will be completed.
Let’s try this type of operation in practice:
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1
operationQueue.addOperation(
CompletionOperation { completion in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
print("First async operation complete")
completion?()
}
print("First sync operation complete")
}
)
operationQueue.addOperation(
CompletionOperation { completion in
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
print("Second async operation complete")
completion?()
}
print("Second sync operation complete")
}
)
As a result, we were able to achieve synchronous execution of Operations
:
First sync operation complete
First async operation complete
Second sync operation complete
Second async operation complete
Don’t hesitate to contact me on
Also published here