paint-brush
OperationQueue + Asynchronous Code: Everything You Need to Knowby@bugorbn
528 reads
528 reads

OperationQueue + Asynchronous Code: Everything You Need to Know

by Boris BugorMarch 17th, 2024
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

In Swift, using an operation queue for synchronous code may seem like pure hell because, under the hood, the code is considered complete if the compilation of their code is completed. By the time the asynchronous code is executed, the `Operation` itself will have already been completed. To understand how to solve the problem, you need to understand how the life cycle of the operation works.
featured image - OperationQueue + Asynchronous Code: Everything You Need to Know
Boris Bugor HackerNoon profile picture

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 Operationitself 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 Twitter if you have any questions. Also, you can always buy me a coffee.


Also published here