Threads

async keyword specifies that function will be executed in background.

await keyword in front of the call allows to mark the possible suspension point.

func listPhotos(inGallery name: String) async -> [String] {
    let result = // ... some asynchronous networking code ...
    return result
}

// wait photos, current execution will be suspended
let photoNames = await listPhotos(inGallery: "Summer Vacation")

To call an asynchronous function and let it run in parallel with code around it, write async in front of let when you define a constant, and then write await each time you use the constant.

async let firstPhoto = downloadPhoto(named: photoNames[0])
async let secondPhoto = downloadPhoto(named: photoNames[1])
async let thirdPhoto = downloadPhoto(named: photoNames[2])

let photos = await [firstPhoto, secondPhoto, thirdPhoto]
show(photos)

Grand Central Dispatch (GCD)

Dispatch framework allows to execute code concurrently on multicore hardware by submitting work to dispatch queues managed by the system. Dispatch, also known as Grand Central Dispatch (GCD)

DispatchQueue is an object that manages the execution of tasks serially or concurrently on your app's main thread or on a background thread.

DispatchQueue.main is predefined queue that is associated with the main thread of the current process.

DispatchQueue.global() allows to get global system queue with the specified quality-of-service (QoS)

let label = UILabel()

DispatchQueue.global(qos: .userInitiated).async {
    let text = loadArticleText() // load text in background

    DispatchQueue.main.async {
        label.text = text // update ui in main thread 
    }
}

You can execute the code after the specified delay.

// execute after 1.5 seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
    // your code here
}

// also .seconds(Int), .microseconds(Int) and .nanoseconds(Int) may be used
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
   // Code
}

You can create own queue.

/* Serial queue with the default QoS, i.e.
any previous work has to be completed
before any new work will begin.
*/
let queueSerial = DispatchQueue(label: "CacheQueue")

/* Concurrent queue with a higher priority, i.e. multiple
 works can be executed simultaneously. 
*/
let queueConcurrent = DispatchQueue(
    label: "ConcurrentQueue",
    qos: .userInitiated, // specify a higher priority
    attributes: [.concurrent] // specify queue must be concurent
)

QoS

A quality-of-service (QoS) class categorizes work to perform on a DispatchQueue. By specifying the quality of a task, you indicate its importance to your app.

There are predefined QoS in DispatchQoS structure.

QoS description
userInteractive The quality-of-service class for user-interactive tasks, such as animations, event handling, or updates to your app's user interface.
userInitiated The quality-of-service class for tasks that prevent the user from actively using your app.
`default` The default quality-of-service class.
utility The quality-of-service class for tasks that the user does not track actively.
background The quality-of-service class for maintenance or cleanup tasks that you create.
unspecified The absence of a quality-of-service class.

operations

iOS has other API to perform tasks asynchronously:

  • Operation - base class to represent task (NSOperation in objective C)
  • InvocationOperation allows to create task from existing method using selector
  • NSBlockOperation allows to create task from the specified block objects. The operation is considered finished when all blocks are executed.
  • OperationQueue allows to regulate the execution of operations

This API uses GCD.

Unlike blocks in GCD, operations can be canceled, have some result and can depend from other operations.

Operations in queue can be suspend.

Create operation from block
queue = OperationQueue()
queue.maxConcurrentOperationCount = 2

// add block as operation
queue.addOperation {
    // ... do something in background
            
    OperationQueue.main.addOperation {
        // ... do something in main thread        
    }
}

queue.cancelAllOperations()

// create block operations explicitly
let operation1 = BlockOperation {
    // ...
}

let operation2 = BlockOperation {
    
}
        
operation2.addDependency(operation1)
operation2.completionBlock = {
    // ...            
}

You can create custom operation from Operation class.

class LoadOperation: Operation {
    
    // will be called in background thread
    override func main() {
        // ... 
    }
}

By default, after the execution of the main() method, the completion block is executed.

But sometimes your task will be more difficult. For example, it may contain nested asynchronous code due to the use of some libraries such as Moya.

To control the execution of an operation, you must override the isExecuting and isFinished properties. In this case don't forget add KVO messages.

How to control the execution of the operation

atomicity

NSLock object allows you to execute code atomically.

var lock = NSLock()
var a = [1, 2, 3]

lock.lock()
    a.append(4)
lock.unlock()

Also you can add convenient extension for NSLock class.

NSLock extension example
extension NSLock {

    @discardableResult
    func withLock<T>(_ block: () throws -> T) rethrows -> T {
        lock()
        defer { unlock() }
        return try block()
    }
}

/* usage
let lock = NSLock()
var a = [1, 2, 3]

lock.with { a.append(4) }
*/

test async

The xCode test API provides expectations which allow you to test asynchronous calls.

Code example
class MyClassTests: XCTestCase {
    
    func testScalingProducesSameAmountOfImages() {
       
        // create an expectation
        let expectation = self.expectation(description: "async")
        
        // call asynchronously something with completion handler
        objTest.asyncCall(){
            // do something ...
            // set ok on completion handler
            expectation.fulfill()
        }
        
        // wait for the expectation to be fullfilled, or time out
        waitForExpectations(timeout: 5, handler: nil)

        // test as usual
        XCTAssertEqual(/*...*/, /*...*/)
    }
}