An Overview of Swift Structured Concurrency in iOS 17

Concurrency can be defined as the ability of software to perform multiple tasks in parallel. Many app development projects will need to use concurrent processing at some point, and concurrency is essential for providing a good user experience. Concurrency, for example, allows an app’s user interface to remain responsive while performing background tasks such as downloading images or processing data.

In this chapter, we will explore the structured concurrency features of the Swift programming language and explain how these can be used to add multi-tasking support to your app projects.

An Overview of Threads

Threads are a feature of modern CPUs and provide the foundation of concurrency in any multitasking operating system. Although modern CPUs can run large numbers of threads, the actual number of threads that can be run in parallel at any one time is limited by the number of CPU cores (depending on the CPU model, this will typically be between 4 and 16 cores). When more threads are required than there are CPU cores, the operating system performs thread scheduling to decide how the execution of these threads is to be shared between the available cores.

Threads can be thought of as mini-processes running within a main process, the purpose of which is to enable at least the appearance of parallel execution paths within application code. The good news is that although structured concurrency uses threads behind the scenes, it handles all of the complexity for you, and you should never need to interact with them directly.

The Application Main Thread

When an app is first started, the runtime system will typically create a single thread in which the app will run by default. This thread is generally referred to as the main thread. The primary role of the main thread is to handle the user interface in terms of UI layout rendering, event handling, and user interaction with views in the user interface.

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Any additional code within an app that performs a time-consuming task using the main thread will cause the entire application to appear to lock up until the task is completed. This can be avoided by launching the tasks to be performed in separate threads, allowing the main thread to continue unhindered with other tasks.

Completion Handlers

As outlined in the chapter entitled “Swift Functions, Methods and Closures”, Swift previously used completion handlers to implement asynchronous code execution. In this scenario, an asynchronous task would be started, and a completion handler would be assigned to be called when the task finishes. In the meantime, the main app code would continue to run while the asynchronous task is performed in the background. On completion of the asynchronous task, the completion handler would be called and passed any results. The body of the completion handler would then execute and process those results.

Unfortunately, completion handlers tend to result in complex and error-prone code constructs that are difficult to write and understand. Completion handlers are also unsuited to handling errors thrown by asynchronous tasks and generally result in large and confusing nested code structures.

Structured Concurrency

Structured concurrency was introduced into the Swift language with Swift version 5.5 to make it easier for app developers to implement concurrent execution safely and in a logical and easy way to both write and understand. In other words, structured concurrency code can be read from top to bottom without jumping back to completion handler code to understand the logic flow. Structured concurrency also makes it easier to handle errors thrown by asynchronous functions.

Swift provides several options for implementing structured concurrency, each of which will be introduced in this chapter.

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Preparing the Project

Launch Xcode and select the option to create a new iOS App project named ConcurrencyDemo. Once created, select the Main.storyboard file, display the Library, and drag a Button object onto the center of the scene layout. Next, double-click on the button and change the text to read “Async Test”, then use the Align menu to add constraints to center the button horizontally and vertically in the container:

Figure 35-1

Display the Assistant Editor and establish an action connection from the button to a method named buttonClick(). Next, edit the ViewController.swift file and add two additional functions that will be used later in the chapter. Finally, modify the buttonCLick() method to call the doSomething function:

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func buttonClick(_ sender: Any) {
        doSomething()
    }
    
    func doSomething() {
    }
 
    func takesTooLong() {
    }
}Code language: Swift (swift)

Non-Concurrent Code

Before exploring concurrency, we will first look at an example of non-concurrent code (also referred to as synchronous code) execution. Begin by adding the following code to the two stub functions. The changes to the doSomething() function print out the current date and time before calling the takesTooLong() function. Finally, the date and time are output once again before the doSomething() function exits.

The takesTooLong() function uses the system sleep() method to simulate the effect of performing a time-consuming task that blocks the main thread until it is complete before printing out another timestamp:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

func doSomething() {
    print("Start \(Date())")
    takesTooLong()
    print("End \(Date())")
}
 
func takesTooLong() {
    sleep(5)
    print("Async task completed at \(Date())")
}Code language: Swift (swift)

Run the app on a device or simulator and click on the “Async Test” button. Output similar to the following should appear in the Xcode console panel:

Start 2023-02-05 17:43:10 +0000
Async task completed at 2023-02-05 17:43:15 +0000
End 2023-02-05 17:43:15 +0000Code language: plaintext (plaintext)

The key point to note in the above timestamps is that the end time is 5 seconds after the start time. This tells us not only that the call to takesTooLong() lasted 5 seconds as expected but that any code after the call was made within the doSomething() function was not able to execute until after the call returned. During that 5 seconds, the app would appear to the user to be frozen.

The answer to this problem is implementing a Swift async/await concurrency structure. Before looking at async/ await, we will need a Swift concurrency-compatible alternative to the sleep() call used in the above example. To achieve this, add a new function to the ViewController.swift file that reads as follows:

func taskSleep(_: Int) async {
    do {
        try await Task.sleep(until: .now + .seconds(5), clock: .continuous)
    } catch { }
}Code language: Swift (swift)

Ironically, the new function used Swift concurrency before we covered the topic. Rest assured, the techniques used in the above code will be explained in the remainder of this chapter.

Introducing async/await Concurrency

The foundation of structured concurrency is the async/await pair. The async keyword is used when declaring a function to indicate that it will be executed asynchronously relative to the thread from which it was called. We need, therefore, to declare both of our example functions as follows (any errors that appear will be addressed later):

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

func doSomething() async {
    print("Start \(Date())")
    takesTooLong()
    print("End \(Date())")
}
 
func takesTooLong() async {
    await taskSleep(5)
    print("Async task completed at \(Date())")
}Code language: Swift (swift)

Marking a function as async achieves several objectives. First, it indicates that the code in the function needs to be executed on a different thread to the one from which it was called. It also notifies the system that the function itself can be suspended during execution to allow the system to run other tasks. As we will see later, these suspend points within an async function are specified using the await keyword.

Another point to note about async functions is that they can generally only be called from within the scope of other async functions though, as we will see later in the chapter, the Task object can be used to provide a bridge between synchronous and asynchronous code. Finally, if an async function calls other async functions, the parent function cannot exit until all child tasks have also been completed.

Most importantly, once a function has been declared asynchronous, it can only be called using the await keyword. Before looking at the await keyword, we must understand how to call async functions from synchronous code.

Asynchronous Calls from Synchronous Functions

The rules of structured concurrency state that an async function can only be called from within an asynchronous context. If the entry point into your program is a synchronous function, this raises the question of how any async functions can ever get called. The answer is to use the Task object from within the synchronous function to launch the async function. Suppose we have a synchronous function named main() from which we need to call one of our async functions and attempt to do so as follows:

func main() {
    doSomething()
}Code language: Swift (swift)

The above code will result in the following error notification in the code editor:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

'async' call in a function that does not support concurrencyCode language: plaintext (plaintext)

The only options we have are to make main() an async function or to launch the function in an unstructured task. Assuming that declaring main() as an async function is not a viable option, in this case, the code will need to be changed as follows:

func main() {
    Task {
        await doSomething()
    }
}Code language: Swift (swift)

The await Keyword

As we previously discussed, the await keyword is required when making a call to an async function and can only usually be used within the scope of another. Attempting to call an async function without the await keyword will result in the following syntax error:

Expression is 'async' but is not marked with 'await'Code language: plaintext (plaintext)

To call the takesTooLong() function, therefore, we need to make the following change to the doSomething() function:

func doSomething() async {
    print("Start \(Date())")
    await takesTooLong()
    print("End \(Date())")
}Code language: Swift (swift)

One more change is now required because we are attempting to call the async doSomething() function from a synchronous context (in this case, the buttonClick action method). To resolve this, we need to use the Task object to launch the doSomething() function:

@IBAction func buttonClick(_ sender: Any) {
    Task {
        await doSomething()
    }
}Code language: Swift (swift)

When tested now, the console output should be similar to the following:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Start 2023-02-05 17:53:03 +0000
Async task completed at 2023-02-05 17:53:08 +0000
End 2023-02-05 17:53:08 +0000Code language: plaintext (plaintext)

This is where the await keyword can be a little confusing. As you have probably noticed, the doSomething() function still had to wait for the takesTooLong() function to return before continuing, giving the impression that the task was still blocking the thread from which it was called. In fact, the task was performed on a different thread, but the await keyword told the system to wait until it completed. The reason for this is that, as previously mentioned, a parent async function cannot complete until all of its sub-functions have also completed. This means that the call has no choice but to wait for the async takesTooLong() function to return before executing the next line of code. The next section will explain how to defer the wait until later in the parent function using the async-let binding expression. Before doing that, however, we need to look at another effect of using the await keyword in this context.

In addition to allowing us to make the async call, the await keyword has also defined a suspend point within the doSomething() function. When this point is reached during execution, it tells the system that the doSomething() function can be temporarily suspended and the thread on which it is running used for other purposes. This allows the system to allocate resources to any higher priority tasks and will eventually return control to the doSomething() function so that execution can continue. By marking suspend points, the doSomething() function is essentially being forced to be a good citizen by allowing the system to briefly allocate processing resources to other tasks. Given the speed of the system, it is unlikely that a suspension will last more than fractions of a second and will not be noticeable to the user while benefiting the overall performance of the app.

Using async-let Bindings

In our example code, we have identified that the default behavior of the await keyword is to wait for the called function to return before resuming execution. A more common requirement, however, is to continue executing code within the calling function while the async function is executing in the background. This can be achieved by deferring the wait until later in the code using an async-let binding. To demonstrate this, we first need to modify our takesTooLong() function to return a result (in this case, our task completion timestamp):

func takesTooLong() async -> Date {
    await taskSleep(5)
    return Date()
}Code language: Swift (swift)

Next, we need to change the call within doSomething() to assign the returned result to a variable using a let expression but also marked with the async keyword:

func doSomething() async {
    print("Start \(Date())")
    async let result = takesTooLong()
    print("End \(Date())")
}Code language: Swift (swift)

Now, we need to specify where within the doSomething() function we want to wait for the result value to be returned. We do this by accessing the result variable using the await keyword. For example:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

func doSomething() async {
    print("Start \(Date())")
    async let result = takesTooLong()
    print("After async-let \(Date())") 
    // Additional code to run concurrently with async function goes here
    print ("result = \(await result)")
    print("End \(Date())")
}Code language: Swift (swift)

When printing the result value, we are using await to let the system know that execution cannot continue until the async takesTooLong() function returns with the result value. At this point, execution will stop until the result is available. However, any code between the async-let and the await will execute concurrently with the takesTooLong() function.

Execution of the above code will generate output similar to the following:

Start 2023-02-05 17:56:00 +0000
After async-let 2023-02-05 17:56:00 +0000
result = 2023-02-05 17:56:05 +0000
End 2023-02-05 17:56:05 +0000Code language: plaintext (plaintext)

Note that the “After async-let” message has a timestamp that is 5 seconds earlier than the “result =” call return stamp confirming that the code was executed while takesTooLong() was also running.

Handling Errors

Error handling in structured concurrency uses the throw/do/try/catch mechanism previously covered in the chapter entitled “Understanding Error Handling in Swift 5”. The following example modifies our original async takesTooLong() function to accept a sleep duration parameter and to throw an error if the delay is outside of a specific range:

enum DurationError: Error {
    case tooLong
    case tooShort
}
.
.
func takesTooLong(delay: Int) async throws {
    
    if delay < 5 {
        throw DurationError.tooShort
    } else if delay > 20 {
        throw DurationError.tooLong
    }
    
    await taskSleep(delay)
    print("Async task completed at \(Date())")
}Code language: Swift (swift)

Now when the function is called, we can use a do/try/catch construct to handle any errors that get thrown:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

func doSomething() async {
    print("Start \(Date())")
    do {
        try await takesTooLong(delay: 25)
    } catch DurationError.tooShort {
        print("Error: Duration too short")
    } catch DurationError.tooLong {
        print("Error: Duration too long")
    } catch {
        print("Unknown error")
    }
    print("End \(Date())")
}Code language: Swift (swift)

When executed, the resulting output will resemble the following:

Start 2022-03-30 19:29:43 +0000
Error: Duration too long
End 2022-03-30 19:29:43 +0000Code language: plaintext (plaintext)

Understanding Tasks

Any work that executes asynchronously runs within an instance of the Swift Task class. An app can run multiple tasks simultaneously and structures these tasks hierarchically. When launched, the async version of our doSomething() function will run within a Task instance. When the takesTooLong() function is called, the system creates a sub-task within which the function code will execute. Regarding the task hierarchy tree, this sub-task is a child of the doSomething() parent task. Any calls to async functions from within the sub-task will become children of that task, and so on.

This task hierarchy forms the basis on which structured concurrency is built. For example, child tasks inherit attributes such as priority from their parents, and the hierarchy ensures that a parent task does not exit until all descendant tasks have been completed.

As we will see later in the chapter, tasks can be grouped to enable the dynamic launching of multiple asynchronous tasks.

Unstructured Concurrency

Individual tasks can be created manually using the Task object, a concept referred to as unstructured concurrency. As we have already seen, a common use for unstructured tasks is to call async functions from within synchronous functions.

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Unstructured tasks also provide more flexibility because they can be externally canceled at any time during execution. This is particularly useful if you need to provide the user with a way to cancel a background activity, such as tapping on a button to stop a background download task. This flexibility comes with extra cost in terms of having to do a little more work to create and manage tasks.

Unstructured tasks are created and launched by calling the Task initializer and providing a closure containing the code to be performed. For example:

Task {
    await doSomething()
}Code language: Swift (swift)

These tasks also inherit the configuration of the parent from which they are called, such as the actor context, priority, and task local variables. Tasks can also be assigned a new priority when they are created, for example:

Task(priority: .high) {
    await doSomething()
}Code language: Swift (swift)

This provides a hint to the system about how the task should be scheduled relative to other tasks. Available priorities ranked from highest to lowest are as follows:

  • .high / .userInitiated
  • .medium
  • .low / .utility
  • .background

When a task is manually created, it returns a reference to the Task instance. This can be used to cancel the task or to check whether the task has already been canceled from outside the task scope:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

let task = Task(priority: .high) {
    await doSomething()
}
.
.          
if (!task.isCancelled) {
    task.cancel()
}Code language: Swift (swift)

Detached Tasks

Detached tasks are another form of unstructured concurrency, but they differ in that they do not inherit any properties from the calling parent. Detached tasks are created by calling the Task.detached() method as follows:

let detachedTask = Task.detached(priority: .medium) {
    await doSomething()
}
.
.          
if (!detachedTask.isCancelled) {
    detachedTask.cancel()
}Code language: Swift (swift)

Task Management

Whether you are using structured or unstructured tasks, the Task class provides a set of static methods and properties that can be used to manage the task from within the scope.

A task may, for example, use the currentPriority property to identify the priority assigned when it was created:

Task {
    let priority = Task.currentPriority
    await doSomething()
}Code language: Swift (swift)

Unfortunately, this is a read-only property so cannot be used to change the priority of the running task. It is also possible for a task to check if it has been canceled by accessing the isCancelled property:

if Task.isCancelled {
    // perform task cleanup
}Code language: Swift (swift)

Another option for detecting cancellation is to call the checkCancellation() method, which will throw a CancellationError error if the task has been canceled:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

A task may cancel itself at any time by calling the cancel() Task method:

do {
    try Task.checkCancellation()
} catch {
   // Perform task cleanup
}Code language: Swift (swift)
Task.cancel()Code language: Swift (swift)

Finally, if there are locations within the task code where execution could safely be suspended, these can be declared to the system via the yield() method:

Task.yield()Code language: Swift (swift)

Working with Task Groups

So far in this chapter, our examples have involved creating one or two tasks (a parent and a child). In each case, we knew how many tasks were required in advance of writing the code. Situations often arise, however, where several tasks need to be created and run concurrently based on dynamic criteria. We might, for example, need to launch a separate task for each item in an array or within the body of a for loop. Swift addresses this by providing task groups.

Task groups allow a dynamic number of tasks to be created and are implemented using either the withThrowingTaskGroup() or withTaskGroup() functions (depending on whether or not the async functions in the group throw errors). The looping construct to create the tasks is then defined within the corresponding closure, calling the group addTask() function to add each new task.

Modify the two functions as follows to create a task group consisting of five tasks, each running an instance of the takesTooLong() function:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

func doSomething() async {
    await withTaskGroup(of: Void.self) { group in
        for i in 1...5 {
            group.addTask {
                let result = await self.takesTooLong()
                print("Completed Task \(i) = \(result)")
            }
        }
    }
}

func takesTooLong() async -> Date {
    await taskSleep(5)
    return Date()
}Code language: Swift (swift)

When executed, there will be a 5-second delay while the tasks run before output similar to the following appears:

Completed Task 1 = 2022-03-31 17:36:32 +0000
Completed Task 2 = 2022-03-31 17:36:32 +0000
Completed Task 5 = 2022-03-31 17:36:32 +0000
Completed Task 3 = 2022-03-31 17:36:32 +0000
Completed Task 4 = 2022-03-31 17:36:32 +0000Code language: plaintext (plaintext)

Note that the tasks all show the same completion timestamp indicating that they were executed concurrently. It is also interesting to notice that the tasks did not complete in the order in which they were launched. When working with concurrency, it is important to remember that there is no guarantee that tasks will be completed in the order they were created.

In addition to the addTask() function, several other methods and properties are accessible from within the task group, including the following:

  • cancelAll() – Method call to cancel all tasks in the group
  • isCancelled – Boolean property indicating whether the task group has already been canceled. • isEmpty – Boolean property indicating whether any tasks remain within the task group.

Avoiding Data Races

In the above task group example, the group did not store the results of the tasks. In other words, the results did not leave the scope of the task group and were not retained when the tasks ended. For example, let’s assume we want to store the task number and result timestamp for each task within a Swift dictionary object (with the task number as the key and the timestamp as the value). When working with synchronous code, we might consider a solution that reads as follows:

func doSomething() async {
 
    var timeStamps: [Int: Date] = [:]
    
    await withTaskGroup(of: Void.self) { group in
        for i in 1...5 {
            group.addTask {
                timeStamps[i] = await self.takesTooLong()
            }
        }
    }
}Code language: Swift (swift)

Unfortunately, the above code will report the following error on the line where the result from the takesTooLong() function is added to the dictionary:

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

Mutation of captured var 'timeStamps' in concurrently-executing codeCode language: plaintext (plaintext)

The problem is that we have multiple tasks concurrently accessing the data and risk encountering a data race condition. A data race occurs when multiple tasks attempt to access the same data concurrently, and one or more of these tasks is performing a write operation. This generally results in data corruption problems that can be hard to diagnose.

One option is to create an actor in which to store the data.

Another solution is to adapt our task group to return the task results sequentially and add them to the dictionary. We originally declared the task group as returning no results by passing Void.self as the return type to the withTaskGroup() function as follows:

await withTaskGroup(of: Void.self) { group in
.
.Code language: Swift (swift)

The first step is to design the task group so that each task returns a tuple containing the task number (Int) and timestamp (Date) as follows. We also need a dictionary in which to store the results:

func doSomething() async {
            
    var timeStamps: [Int: Date] = [:]
 
    await withTaskGroup(of: (Int, Date).self) { group in       
        for i in 1...5 {
            group.addTask {
                return(i, await self.takesTooLong())
            }
        }
    }
}Code language: Swift (swift)

Next, we need to declare a second loop to handle the results as they are returned from the group. Because the results are being returned individually from async functions, we cannot simply write a loop to process them all at once. Instead, we need to wait until each result is returned. For this situation, Swift provides the for-await loop.

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

The for-await Loop

The for-await expression allows us to step through sequences of values that are being returned asynchronously and await the receipt of values as they are returned by concurrent tasks. The only requirement for using forawait is that the sequential data conforms to the AsyncSequence protocol (which should always be the case when working with task groups).

In our example, we need to add a for-await loop within the task group scope, but after the addTask loop as follows:

func doSomething() async {
    
    var timeStamps: [Int: Date] = [:]
    
    await withTaskGroup(of: (Int, Date).self) { group in
        
        for i in 1...5 {
            group.addTask {
                return(i, await self.takesTooLong())
            }
        }
        
        for await (task, date) in group {
            timeStamps[task] = date
        }
    }
}Code language: Swift (swift)

As each task returns, the for-await loop will receive the resulting tuple and store it in the timeStamps dictionary. To verify this, we can add some code to print the dictionary entries after the task group exits:

func doSomething() async {
.
.
        for await (task, date) in group {
            timeStamps[task] = date
        }
    }
    
    for (task, date) in timeStamps {
        print("Task = \(task), Date = \(date)")
    }
}Code language: Swift (swift)

When executed, the output from the completed example should be similar to the following:

Task = 2, Date = 2023-02-05 18:54:06 +0000
Task = 3, Date = 2023-02-05 18:54:06 +0000
Task = 4, Date = 2023-02-05 18:54:06 +0000
Task = 5, Date = 2023-02-05 18:54:06 +0000
Task = 1, Date = 2023-02-05 18:54:06 +0000Code language: plaintext (plaintext)

Asynchronous Properties

In addition to async functions, Swift also supports async properties within class and struct types. Asynchronous properties are created by explicitly declaring a getter and marking it as async as demonstrated in the following example. Currently, only read-only properties can be asynchronous.

 

You are reading a sample chapter from Building iOS 17 Apps using Xcode Storyboards.

Buy the full book now in eBook or Print format.

The full book contains 96 chapters and 760 pages of in-depth information.

Learn more.

Preview  Buy eBook  Buy Print

 

struct MyStruct {
    var myResult: Date {
        get async {
            return await self.getTime()
        }
    }
    func getTime() async -> Date {
        sleep(5)
        return Date()
    }
}
.
.
func doSomething() async {
 
    let myStruct = MyStruct()
 
    Task {
        let date = await myStruct.myResult
        print(date)
    }
}Code language: Swift (swift)

Summary

Modern CPUs and operating systems are designed to execute code concurrently, allowing multiple tasks to be performed simultaneously. This is achieved by running tasks on different threads, with the main thread primarily responsible for rendering the user interface and responding to user events. By default, most code in an app is also executed on the main thread unless specifically configured to run on a different thread. If that code performs tasks that occupy the main thread for too long, the app will appear to freeze until the task completes. To avoid this, Swift provides the structured concurrency API. When using structured concurrency, code that would block the main thread is instead placed in an asynchronous function (async properties are also supported) so that it is performed on a separate thread. The calling code can be configured to wait for the async code to complete before continuing using the await keyword or to continue executing until the result is needed using async-let.

Tasks can be run individually or as groups of multiple tasks. The for-await loop provides a useful way to asynchronously process the results of asynchronous task groups.


Categories