A SwiftUI Core Data Tutorial

Now that we have explored the concepts of Core Data it is time to put that knowledge to use by creating an example app project. In this project tutorial, we will be creating a simple inventory app that uses Core Data to persistently store the names and quantities of products. This will include the ability to add, delete, and search for database entries.

Creating the CoreDataDemo Project

Launch Xcode, select the option to create a new project and choose the Multiplatform App template before clicking the Next button. On the project options screen, name the project CoreDataDemo and choose an organization identifier that will uniquely identify your app (this will be important when we add CloudKit support to the project in a later chapter).

Note that the options screen includes a Use Core Data setting as highlighted in Figure 44-1. This setting does the work of setting up the project for Core Data support and generates code to implement a simple app that demonstrates Core Data in action. Instead of using this template, this tutorial will take you through the steps of manually adding Core Data support to a project so that you have a better understanding of how Core Data works. For this reason, make sure the Use Core Data option is turned off before clicking the Next button:

Figure 44-1

Select a suitable location in which to save the project before clicking on the Finish button.

Defining the Entity Description

For this example, the entity takes the form of a data model designed to hold the names and quantities that will make up the product inventory. Right-click on the Shared folder within the project navigator and select the New File… option from the menu when it appears. Within the template dialog, select the Data Model entry located in the Core Data section as shown in Figure 44-2, then click the Next button:

Figure 44-2

Name the file Products and click on the Create button to generate the file. Once the file has been created, it will appear within the entity editor as shown below:

Figure 44-3

To add a new entity to the model, click on the Add Entity button marked A in Figure 44-3 above. Xcode will add a new entity (named Entity) to the model and list it beneath the Entities heading (B). Click on the new entity and change the name to Product:

Figure 44-4

Now that the entity has been created, the next step is to add the name and quantity attributes. To add the first attribute, click on the + button located beneath the Attributes section of the main panel. Name the new attribute name and change the Type to String as shown in Figure 44-5:

Figure 44-5

Repeat these steps to add a second attribute of type String named quantity. Upon completion of these steps, the attributes panel should match Figure 44-6:

Figure 44-6

Creating the Persistence Controller

The next requirement for our project is a persistence controller class in which to create and initialize an NSPersistentContainer instance. Right-click once again on the Shared folder in the project navigator and select the New File… menu option. Select the Swift File template option and save it as Persistence.swift. With the new file loaded into the code editor, modify it so that it reads as follows:

import CoreData
 
struct PersistenceController {
    static let shared = PersistenceController()
    
    let container: NSPersistentContainer
 
    init() {
        container = NSPersistentContainer(name: "Products")
        
        container.loadPersistentStores { (storeDescription, error) in
            if let error = error as NSError? {
                fatalError("Container load failed: \(error)")
            }
        }
    }
}

Setting up the View Context

Now that we have created a persistent controller we can use it to obtain a reference to the view context. An ideal place to perform this task is within the CoreDataDemoApp.swift file. To make the context accessible to the views that will make up the app, we will insert it into the view hierarchy as an environment object as follows:

import SwiftUI
 
@main
struct CoreDataDemoApp: App {
    
    let persistenceController = PersistenceController.shared
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, 
                             persistenceController.container.viewContext)
        }
    }
}

Preparing the ContentView for Core Data

Before we start adding views to design the app user interface, the following initial changes are required within the ContentView.swift file:

import SwiftUI
import CoreData
 
struct ContentView: View {
    
    @State var name: String = ""
    @State var quantity: String = ""
    
    @Environment(\.managedObjectContext) private var viewContext
    
    @FetchRequest(entity: Product.entity(), sortDescriptors: [])
    private var products: FetchedResults<Product>
    
    var body: some View {
.
.

In addition to importing the CoreData library, we have also declared two state objects into which will be stored the product name and quantity as they are entered by the user. We have also gained access to the view context environment object that was created in the CoreDataDemoApp.swift file.

The @FetchRequest property wrapper is also used to declare a variable named products into which Core Data will store the latest product data stored in the database.

Designing the User Interface

With most of the preparatory work complete, we can now begin designing the layout of the main content view. Remaining in the ContentView.swift file, modify the body of the ContentView structure so that it reads as follows:

.
.
   var body: some View {
        NavigationView {
            VStack {
                TextField("Product name", text: $name)
                TextField("Product quantity", text: $quantity)
                
                HStack {
                    Spacer()
                    Button("Add") {
                        
                    }
                    Spacer()
                    Button("Clear") {
                        name = ""
                        quantity = ""
                    }
                    Spacer()
                }
                .padding()
                .frame(maxWidth: .infinity)
                
                List {
                    ForEach(products) { product in
                        HStack {
                            Text(product.name ?? "Not found")
                            Spacer()
                            Text(product.quantity ?? "Not found")
                        }
                    }
                }
                .navigationTitle("Product Database")
            }
            .padding()
            .textFieldStyle(RoundedBorderTextFieldStyle())
        }
    }
.
.

The layout initially consists of two TextField views, two Buttons, and a List which should render within the preview canvas as follows:

Figure 44-7

Saving Products

More code changes are now required so that data entered into the product name and quantity text fields is saved by Core Data into persistent storage when the Add button is clicked. Edit the ContentView.swift file once again to add this functionality:

.
.
   var body: some View {
        NavigationView {
            VStack {
                TextField("Product name", text: $name)
                TextField("Product quantity", text: $quantity)
                
                HStack {
                    Spacer()
                    Button("Add") {
                        addProduct()
                    }
                    Spacer()
                    Button("Clear") {
                        name = ""
                        quantity = ""
                    }
.
.
            .padding()
            .textFieldStyle(RoundedBorderTextFieldStyle())
        }
    }
    
    private func addProduct() {
        
        withAnimation {
            let product = Product(context: viewContext)
            product.name = name
            product.quantity = quantity
            
            saveContext()
        }
    }
    
    private func saveContext() {
        do {
            try viewContext.save()
        } catch {
            let error = error as NSError
            fatalError("An error occured: \(error)")
        }
    }
}
.
.

The first change configured the Add button to the call a function named addProduct() which was declared as follows:

private func addProduct() {
    
    withAnimation {
        let product = Product(context: viewContext)
        product.name = name
        product.quantity = quantity
        
        saveContext()
    }
}

The addProduct() function creates a new Product entity instance and assigns the current content of the product name and quantity state properties to the corresponding entity attributes. A call is then made to the following saveContext() function:

private func saveContext() {
    do {
        try viewContext.save()
    } catch {
        let error = error as NSError
        fatalError("An error occured: \(error)")
    }
}

The saveContext() function uses a “do.. try .. catch” construct to save the current viewContext to persistent storage. For testing purposes, a fatal error is triggered to terminate the app if the save action failed. More comprehensive error handling would typically be required for a production-quality app.

Saving the data will cause the latest data to be fetched and assigned to the products data variable. This, in turn, will cause the List view to update with the latest products. To make this update visually appealing, the code in the addProduct() function is placed in a withAnimation call.

Testing the addProduct() Function

Compile and run the app on a device or simulator, enter a few product and quantity entries, and verify that those entries appear in the List view as they are added. After entering information into the text fields, check that clicking on the Clear button clears the current entries.

At this point in the tutorial, the running app should resemble that shown in Figure 44-8 after some products have been added:

Figure 44-8

To make the list more organized, the product items need to be sorted in ascending alphabetical order based on the name attribute. To implement this, add a sort descriptor to the @FetchRequest definition as outlined below. This requires the creation of an NSSortDescriptor instance configured with the name attribute declared as the key and the ascending property set to true:

@FetchRequest(entity: Product.entity(), 
           sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)])
private var products: FetchedResults<Product>

When the app is now run, the list of products will be sorted in ascending alphabetic order.

Deleting Products

Now that the app has a mechanism for adding product entries to the database, we need a way to delete entries that are no longer needed. For this project, we will use the same steps demonstrated in the chapter entitled SwiftUI Lists and Navigation. This will allow the user to delete entries by swiping on the list item and tapping the delete button. Beneath the existing addProduct() function, add a new function named deleteProduct() that reads as follows:

private func deleteProducts(offsets: IndexSet) {
    withAnimation {
        offsets.map { products[$0] }.forEach(viewContext.delete)
            saveContext()
        }
}

When the method is called, it is passed a set of offsets within the List entries representing the positions of the items selected by the user for deletion. The above code loops through these entries calling the viewContext delete() function for each deleted item. Once the deletions are complete, the changes are saved to the database via a call to our saveContext() function.

Now that we have added the deleteProduct() function, the List view can be modified to call it via the onDelete() modifier:

.
.
       List {
            ForEach(products) { product in
                HStack {
                    Text(product.name ?? "Not found")
                    Spacer()
                    Text(product.quantity ?? "Not found")
                }
            }
            .onDelete(perform: deleteProducts)
        }
        .navigationTitle("Product Database")
.
.

Run the app and verify both that performing a leftward swipe on a list item reveals the delete option and that clicking it removes the item from the list.

Figure 44-9

Adding the Search Function

The final feature to be added to the project will allow us to search the database for products that match the text entered into the name text field. The results will appear in a list contained within a second view named ResultsView. When it is called from ContentView, ResultsView will be passed the current value of the name state property and a reference to the viewContext object.

Begin by adding the ResultsView structure to the ContentView.swift file as follows:

struct ResultsView: View {
    
    var name: String
    var viewContext: NSManagedObjectContext
    @State var matches: [Product]?
 
    var body: some View {
       
        return VStack {
            List {
                ForEach(matches ?? []) { match in
                    HStack {
                        Text(match.name ?? "Not found")
                        Spacer()
                        Text(match.quantity ?? "Not found")
                    }
                }
            }
            .navigationTitle("Results")   
        }
    }
}

In addition to the name and viewContext parameters, the declaration also includes a state property named matches into which will be placed the matching product search results which, in turn, will be displayed within the List view.

We now need to add some code to perform the search and will do so by applying a task() modifier to the VStack container view. This will ensure that search is performed asynchronously and that all of the view’s properties have been initialized before the search is executed:

.
.
    return VStack {
        List {
      
            ForEach(myMatches ?? []) { match in
                HStack {
                    Text(match.name ?? "Not found")
                    Spacer()
                    Text(match.quantity ?? "Not found")
                }
            }
        }
        .navigationTitle("Results")
       
    }
    .task {
        let fetchRequest: NSFetchRequest<Product> = Product.fetchRequest()
        
        fetchRequest.entity = Product.entity()
        fetchRequest.predicate = NSPredicate(
            format: "name CONTAINS %@", name
        )
        matches = try? viewContext.fetch(fetchRequest)
    }
.
.

So that the search finds all products that contain the specified text, the predicate is configured using the CONTAINS keyword. This provides more flexibility than performing exact match searches using the LIKE keyword by finding partial matches.

The code in the closure of the task() modifier obtains an NSFetchRequest instance from the Product entity and assigns it an NSPredicate instance configured to find matches between the name variable and the name product entity attribute. The fetch request is then passed to the fetch() method of the view context, and the results assigned to the matches state object. This, in turn, will cause the List to be re-rendered with the matching products.

The last task before testing the search feature is to add a navigation link to ResultsView, keeping in mind that ResultsView is expecting to be passed the name state object and a reference to viewContext. This needs to be positioned between the Add and Clear buttons as follows:

.
.
   HStack {
        Spacer()
        Button("Add") {
            addProduct()
        }
        Spacer()
        NavigationLink(destination: ResultsView(name: name, 
                       viewContext: viewContext)) {
            Text("Find")
        }
        Spacer()
        Button("Clear") {
            name = ""
            quantity = ""
        }
        Spacer()
    } 
.
.

Check the preview canvas to confirm that the navigation link appears as shown in Figure 44-10:

Figure 44-10

Testing the Completed App

Run the app once again and add some additional products, preferably with some containing the same word. Enter the common word into the name text field and click on the Find link. The ResultsView screen should appear with a list of matching items. Figure 44-11, for example, illustrates a search performed on the word “Milk”:

Figure 44-11

Summary

In this chapter, we have used Core Data to provide persistent database storage within an app project. Topics covered include the creation of a Core Data entity model and the configuration of entity attributes. Steps were also taken to initialize a persistent container from which we obtained the view context. The project also used the @FetchRequest property wrapper configured to store entries in alphabetical order and also made use of the view context to add, delete, and search for database entries. In implementing the search behavior, we used an NSFetchRequest instance configured with an NSPredicate object and passed that to the fetch() method of the view context to find matching results.

An Introduction to Core Data and SwiftUI

A common requirement when developing iOS apps is to store data in some form of structured database. One option is to directly manage data using an embedded database system such as SQLite. While this is a perfectly good approach for working with SQLite in many cases, it does require knowledge of SQL and can lead to some complexity in terms of writing code and maintaining the database structure. This complexity is further compounded by the non-object-oriented nature of the SQLite API functions. In recognition of these shortcomings, Apple introduced the Core Data Framework. Core Data is essentially a framework that places a wrapper around the SQLite database (and other storage environments) enabling the developer to work with data in terms of Swift objects without requiring any knowledge of the underlying database technology.

We will begin this chapter by defining some of the concepts that comprise the Core Data model before providing an overview of the steps involved in working with this framework. Once these topics have been covered, the next chapter will work through a SwiftUI Core Data tutorial.

The Core Data Stack

Core Data consists of several framework objects that integrate to provide the data storage functionality. This stack can be visually represented as illustrated in Figure 43-1:

Figure 43-1

As we can see from Figure 43-1, the app sits on top of the stack and interacts with the managed data objects handled by the managed object context. Of particular significance in this diagram is the fact that although the lower levels in the stack perform a considerable amount of the work involved in providing Core Data functionality, the application code does not interact with them directly.

Before moving on to the more practical areas of working with Core Data it is important to spend some time explaining the elements that comprise the Core Data stack in a little more detail.

Persistent Container

The persistent container handles the creation of the Core Data stack and is designed to be easily subclassed to add additional application-specific methods to the base Core Data functionality. Once initialized, the persistent container instance provides access to the managed object context.

Managed Objects

Managed objects are the objects that are created by your application code to store data. A managed object may be thought of as a row or a record in a relational database table. For each new record to be added, a new managed object must be created to store the data. Similarly, retrieved data will be returned in the form of managed objects, one for each record matching the defined retrieval criteria. Managed objects are instances of the NSManagedObject class, or a subclass thereof. These objects are contained and maintained by the managed object context.

Managed Object Context

Core Data-based applications never interact directly with the persistent store. Instead, the application code interacts with the managed objects contained in the managed object context layer of the Core Data stack. The context maintains the status of the objects in relation to the underlying data store and manages the relationships between managed objects defined by the managed object model. All interactions with the underlying database are held temporarily within the context until the context is instructed to save the changes, at which point the changes are passed down through the Core Data stack and written to the persistent store.

Managed Object Model

So far we have focused on the management of data objects but have not yet looked at how the data models are defined. This is the task of the Managed Object Model which defines a concept referred to as entities.

Much as a class description defines a blueprint for an object instance, entities define the data model for managed objects. In essence, an entity is analogous to the schema that defines a table in a relational database. As such, each entity has a set of attributes associated with it that define the data to be stored in managed objects derived from that entity. For example, a Contacts entity might contain name, address, and phone number attributes.

In addition to attributes, entities can also contain relationships, fetched properties, persistent stores, and fetch requests:

  • Relationships – In the context of Core Data, relationships are the same as those in other relational database systems in that they refer to how one data object relates to another. Core Data relationships can be one-to-one, one-to-many, or many-to-many.
  • Fetched property – This provides an alternative to defining relationships. Fetched properties allow properties of one data object to be accessed from another data object as though a relationship had been defined between those entities. Fetched properties lack the flexibility of relationships and are referred to by Apple’s Core Data documentation as “weak, one-way relationships” best suited to “loosely coupled relationships”.
  • Fetch request – A predefined query that can be referenced to retrieve data objects based on defined predicates. For example, a fetch request can be configured into an entity to retrieve all contact objects where the name field matches “John Smith”.

Persistent Store Coordinator

The persistent store coordinator is responsible for coordinating access to multiple persistent object stores. As an iOS developer, you will never directly interact with the persistent store coordinator and will very rarely need to develop an application that requires more than one persistent object store. When multiple stores are required, the coordinator presents these stores to the upper layers of the Core Data stack as a single store.

Persistent Object Store

The term persistent object store refers to the underlying storage environment in which data are stored when using Core Data. Core Data supports three disk-based and one memory-based persistent store. Disk-based options consist of SQLite, XML, and binary. By default, iOS will use SQLite as the persistent store. In practice, the type of store being used is transparent to you as the developer. Regardless of your choice of persistent store, your code will make the same calls to the same Core Data APIs to manage the data objects required by your application.

Defining an Entity Description

Entity descriptions may be defined from within the Xcode environment. When a new project is created with the option to include Core Data, a template file will be created named <entityname>.xcdatamodeld. Xcode also provides a way to manually add entity description files to existing projects. Selecting this file in the Xcode project navigator panel will load the model into the entity editing environment as illustrated in Figure 43-2:

Figure 43-2

Create a new entity by clicking on the Add Entity button located in the bottom panel. The new entity will appear as a text box in the Entities list. By default, this will be named Entity. Double-click on this name to change it.

To add attributes to the entity, click on the Add Attribute button located in the bottom panel, or use the + button located beneath the Attributes section. In the Attributes panel, name the attribute and specify the type and any other options that are required.

Repeat the above steps to add more attributes and additional entities.

The Xcode entity editor also allows relationships to be established between entities. Assume, for example, two entities named Contacts and Sales. To establish a relationship between the two tables select the Contacts entity and click on the + button beneath the Relationships panel. In the detail panel, name the relationship, specify the destination as the Sales entity, and any other options that are required for the relationship. Once the relationship has been established it is, perhaps, best viewed graphically by selecting the Table, Graph option in the Editor Style control located in the bottom panel:

Figure 43-3

Initializing the Persistent Container

The persistent container is initialized by creating a new NSPersistentContainer instance, passing through the name of the model to be used, and then making a call to the loadPersistentStores method of that object as follows:

let persistentContainer: NSPersistentContainer
 
persistentContainer = NSPersistentContainer(name: "DemoData")
persistentContainer.loadPersistentStores { (storeDescription, error) in
    if let error = error as NSError? {
        fatalError("Container load failed: \(error)")
    }
}

Obtaining the Managed Object Context

Since many of the Core Data methods require the managed object context as an argument, the next step after defining entity descriptions often involves obtaining a reference to the context. This can be achieved by accessing the viewContext property of the persistent container instance:

let managedObjectContext = persistentContainer.viewContext

Setting the Attributes of a Managed Object

As previously discussed, entities and the managed objects from which they are instantiated contain data in the form of attributes. Once a managed object instance has been created as outlined above, those attribute values can be used to store the data before the object is saved. Assuming a managed object named contact with attributes named name, address and phone respectively, the values of these attributes may be set as follows before saving the object to storage:

contact.name = "John Smith" 
contact.address = "1 Infinite Loop" 
contact.phone = "555-564-0980"

Saving a Managed Object

Once a managed object instance has been created and configured with the data to be stored it can be saved to storage using the save() method of the managed object context as follows:

do {
    try viewContext.save()
} catch {
    let error = error as NSError
    fatalError("An error occured: \(error)")
}

Fetching Managed Objects

Once managed objects are saved into the persistent object store those objects and the data they contain will likely need to be retrieved. One way to fetch data from Core Data storage is to use the @FetchRequest property wrapper when declaring a variable in which to store the data. The following code, for example, declares a variable named customers which will be automatically updated as data is added to or removed from the database:

@FetchRequest(entity: Customer.entity(), sortDescriptors: [])
private var customers: FetchedResults<Customer>

The @FetchRequest property wrapper may also be configured to sort the fetched results. In the following example, the customer data stored in the customers variable will be sorted alphabetically in ascending order based on the name entity attribute:

@FetchRequest(entity: Customer.entity(), 
        sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)])
private var customers: FetchedResults<Customer>

Retrieving Managed Objects based on Criteria

The preceding example retrieved all of the managed objects from the persistent object store. More often than not only managed objects that match specified criteria are required during a retrieval operation. This is performed by defining a predicate that dictates criteria that a managed object must meet to be eligible for retrieval. For example, the following code configures a @FetchRequest property wrapper declaration with a predicate to extract only those managed objects where the name attribute matches “John Smith”:

@FetchRequest(
  entity: Customer.entity(),
  sortDescriptors: [],
  predicate: NSPredicate(format: "name LIKE %@", "John Smith")
) 
private var customers: FetchedResults<Customer>

The above example will maintain the customers variable so that it always contains the entries that match the specified predicate criteria. It is also possible to perform one-time fetch operations by creating NSFetchRequest instances, configuring them with the entity and predicate settings, and then passing them to the fetch() method of the managed object context. For example:

@State var matches: [Customer]?
let fetchRequest: NSFetchRequest<Product> = Product.fetchRequest()
 
fetchRequest.entity = Customer.entity()
fetchRequest.predicate = NSPredicate(
    format: "name LIKE %@", "John Smith"
)
 
matches = try? viewContext.fetch(fetchRequest)

Summary

The Core Data Framework stack provides a flexible alternative to directly managing data using SQLite or other data storage mechanisms. By providing an object-oriented abstraction layer on top of the data the task of managing data storage is made significantly easier for the SwiftUI application developer. Now that the basics of Core Data have been covered, the next chapter entitled “A SwiftUI Core Data Tutorial” will work through the creation of an example application.

An Introduction to Swift Actors

Structured concurrency in Swift provides a powerful platform for performing multiple tasks at the same time, greatly increasing app performance and responsiveness. One of the downsides of concurrency is that it can lead to problems when multiple tasks access the same data concurrently, and that access includes a mix of reading and writing operations. This type of problem is referred to as a data race and can lead to intermittent crashes and unpredictable app behavior.

In the previous chapter, we looked at a solution to this problem that involved sequentially processing the results from multiple concurrent tasks. Unfortunately, that solution only really works when the tasks involved all belong to the same task group. A more flexible solution and one that works regardless of where the concurrent tasks are launched involves the use of Swift actors.

An Overview of Actors

Actors are a Swift type that controls asynchronous access to internal mutable state so that only one task at a time can access data. Actors are much like classes in that they are a reference type and contain properties, initializers, and methods. Like classes, actors can also conform to protocols and be enhanced using extensions. The primary difference when declaring an actor is that the word “actor” is used in place of “class”.

Declaring an Actor

A simple Swift class that contains a property and a method might be declared as follows:

class BuildMessage {
    
    var message: String = ""
    let greeting = "Hello"
    
    func setName(name: String) {
        self.message = "\(greeting) \(name)"
    }
}

To make the class into an actor we just need to change the type declaration from “class” to “actor”:

actor BuildMessage {
    
    var message: String = ""
    let greeting = "Hello"
    
    func setName(name: String) {
        self.message = "\(greeting) \(name)"
    }
}

Once declared, actor instances are created in the same way as classes, for example:

let hello = BuildMessage()

A key difference between classes and actors, however, is that actors can only be created and accessed from within an asynchronous context, such as within an async function or Task closure. Also, when calling an actor method or accessing a property, the await keyword must be used, for example:

func someFunction() async {
    let builder = BuildMessage()
    await builder.setName(name: "Jane Smith")
    let message = await builder.message
    print(message)
}

Understanding Data Isolation

The data contained in an actor instance is said to be isolated from the rest of the code in the app. This isolation is imposed in part by ensuring that when a method that changes the instance data (in this case, changing the name variable) is called, the method is executed to completion before the method can be called from anywhere else in the code. This prevents multiple tasks from concurrently attempting to change the data. This, of course, means that method calls and property accesses may have to wait until a previous task has been handled, hence the need for the await statement.

Isolation also prevents code from directly changing the mutable internal properties of an actor. Consider, for example, the following code to directly assign a new value to the message property of our BuildMessage instance.

builder.message = "hello"

Though valid when working with class instances, the above code will generate the following error when attempted on an actor instance:

Actor-isolated property ‘message’ can not be mutated from a non-isolated context

By default, all methods and mutable properties within an actor are considered to be isolated and, as such, can only be called using the await keyword. Actor methods that do not access mutable properties may be excluded from isolation using the nonisolated keyword. Once declared in this way, the method can be called without the await keyword, and also called from synchronous code. We can, for example, add a nonisolated method to our BuildMessage actor that returns the greeting string:

actor BuildMessage {
    
    var message: String = ""
    let greeting = "Hello"
    
    func setName(name: String) {
        self.message = "\(greeting) \(name)"
    }
    
    nonisolated func getGreeting() -> String {
        return greeting
    }
}

This new method can be called without the await keyword from both synchronous and asynchronous contexts:

var builder = BuildMessage()
 
func asyncFunction() async {
    let greeting = builder.getGreeting()
    print(greeting)
}
 
func syncFunction() {
    let greeting = builder.getGreeting()
    print(greeting)
}

It is only possible to declare getGreeting() as nonisolated because the method only accesses the immutable greeting property. If the method attempted to access the mutable message property, the following error would be reported:

Actor-isolated property 'message' can not be referenced from a non-isolated context

Note that although immutable properties are excluded from isolation by default, the nonisolated keyword may still be declared for clarity:

nonisolated let greeting = "Hello"

A Swift Actor Example

In the previous chapter, we looked at data races and explored how the compiler will prevent us from writing code that could cause one with the following error message:

Mutation of captured var ‘timeStamps’ in concurrently-executing code

We encountered this error when attempting to write entries to a dictionary object using the following asynchronous code:

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

One option to avoid this problem, and the one implemented in the previous chapter, was to process the results from the asynchronous tasks sequentially using a for-await loop. As we have seen in this chapter, however, the problem could also be resolved by making use of an actor.

With the ConcurrencyDemo project loaded into Xcode, edit the ContentView.swift file to add the following actor declaration is used to encapsulate the timeStamps dictionary and to provide a method via which data can be added:

import SwiftUI
 
actor TimeStore {
    
    var timeStamps: [Int: Date] = [:]
    
    func addStamp(task: Int, date: Date) {
        timeStamps[task] = date
    }
}
.
.

Having declared the actor, we can now modify the doSomething() method to add new timestamps via the addStamp() method:

func doSomething() async {
 
    let store = TimeStore()
    
    await withTaskGroup(of: Void.self) { group in
        for i in 1...5 {
            group.addTask {
                await store.addStamp(task: i, date: await takesTooLong())
            }
        }
    }
 
    for (task, date) in await store.timeStamps {
            print("Task = \(task), Date = \(date)")
    }
}
 
func takesTooLong() async -> Date {
    Thread.sleep(forTimeInterval: 5)
    return Date()
}

With these changes made, the code should now compile and run without error.

Introducing the MainActor

In the chapter entitled An Overview of Swift Structured Concurrency, we talked about the main thread (or main queue) and explained how it is responsible both for handling the rendering of the UI and also responding to user events. We also demonstrated the risks of performing thread blocking tasks on the main thread and how doing so can cause the running program to freeze. As we have also seen, Swift provides a simple and powerful mechanism for running tasks on separate threads from the main thread. What we have not mentioned yet is that it is also essential that updates to the UI are only performed on the main thread. Performing UI updates on any separate thread other than the main thread is likely to cause instability and unpredictable app behavior that can be difficult to debug.

Within Swift, the main thread is represented by the main actor. This is referred to as a global actor because it is An Introduction to Swift Actors accessible throughout your program code when you need code to execute on the main thread.

When developing your app, situations may arise where you have code that you want to run on the main actor, particularly if that code updates the UI in some way. In this situation, the code can be marked using the @ MainActor attribute. This attribute may be used on types, methods, instances, functions, and closures to indicate that the associated operation must be performed on the main actor. We could, for example, configure a class so that it operates only on the main thread:

@MainActor
class TimeStore {
    
    var timeStamps: [Int: Date] = [:]
    
    func addStamp(task: Int, date: Date) {
        timeStamps[task] = date
    }
}

Alternatively, a single value or property can be marked as being main thread dependent:

class TimeStore {
    
    @MainActor var timeStamps: [Int: Date] = [:]
 
    func addStamp(task: Int, date: Date) {
        timeStamps[task] = date
    }
}

Of course, now that the timeStamps dictionary is assigned to the main actor, it cannot be accessed on any other thread. The attempt to add a new date to the dictionary in the above addStamp() method will generate the following error:

Property 'timeStamps' isolated to global actor ‘MainActor’ can not be mutated from this context

To resolve this issue, the addStamp() method must also be marked using the @MainActor attribute:

@MainActor func addStamp(task: Int, date: Date) {
    timeStamps[task] = date
}

The run method of the MainActor can also be called directly from within asynchronous code to perform tasks on the main thread as follows:

func runExample() async {
 
    await MainActor.run {
        // Perform tasks on main thread
    }
}

Summary

A key part of writing asynchronous code is avoiding data races. A data race occurs when two or more tasks access the same data and at least one of those tasks performs a write operation. This can cause data inconsistencies where the concurrent tasks see and act on different versions of the same data.

A useful tool for avoiding data races is the Swift Actor type. Actors are syntactically and behaviorally similar to Swift classes but differ in that the data they encapsulate is said to be isolated from the rest of the code in the app. If a method within an actor that changes instance data is called, that method is executed to completion before the method can be called from anywhere else in the code. This prevents multiple tasks from concurrently attempting to change the data. Actor method calls and property accesses must be called using the await keyword.

The main actor is a special actor that provides access to the main thread from within asynchronous code. The @ MainActor attribute can be used to mark types, methods, instances, functions, and closures to indicate that the associated task must be performed on the main thread.

An Overview of Swift Structured Concurrency

Concurrency can be defined as the ability of software to perform multiple tasks in parallel. Many app development projects will need to make use of concurrent processing at some point and concurrency is essential for providing a good user experience. Concurrency, for example, is what allows the user interface of an app 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.

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 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 the 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 way that is logical and easy to both write and understand. In other words, structured concurrency code can be read from top to bottom without having to jump 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.

Preparing the Project

Launch Xcode and select the option to create a new Multiplatform App project named ConcurrencyDemo. Once created, edit the ContentView.swift file so that it reads as follows:

import SwiftUI
 
struct ContentView: View {
    var body: some View {
        Button(action: {
            doSomething()
        }) {
            Text("Do Something")
        }
    }
    
    func doSomething() {
    }
 
    func takesTooLong() {
    }    
}
 
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

The above changes create a Button view configured to call the doSomething() function when clicked. In the remainder of this chapter, we will make changes to this template code to demonstrate structured concurrency in 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 sleep() method of the Thread object to simulate the effect of performing a time consuming task that blocks the main thread until it is complete before printing out another timestamp:

func doSomething() {
    print("Start \(Date())")
    takesTooLong()
    print("End \(Date())")
}
 
func takesTooLong() {
    Thread.sleep(forTimeInterval: 5)
    print("Async task completed at \(Date())")
}

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

Start 2022-03-29 17:55:16 +0000
Async task completed at 2022-03-29 17:55:21 +0000
End 2022-03-29 17:55:21 +0000

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 to implement a Swift async/await concurrency structure.

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 is to 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):

func doSomething() async {
    print("Start \(Date())")
    takesTooLong()
    print("End \(Date())")
}
 
func takesTooLong() async {
    Thread.sleep(forTimeInterval: 5)
    print("Async task completed at \(Date())")
}

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 completed.

Most importantly, once a function has been declared as being asynchronous, it can only be called using the await keyword. Before looking at the await keyword, we need to 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()
}

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

'async' call in a function that does not support concurrency

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()
    }
}

The await Keyword

As we have 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 async function. 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'

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())")
}

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

var body: some View {
    Button(action: {
        Task {
            await doSomething()
        }
    }) {
        Text("Do Something")
    }
}

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

Start 2022-03-30 13:27:42 +0000
Async task completed at 2022-03-30 13:27:47 +0000
End 2022-03-30 13:27:47 +0000

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. The call, therefore, has no choice but to wait for the async takesTooLong() function to return before executing the next line of code. In the next section, we will explain how to defer the wait until later in the parent function using the async-let binding expression. Before doing so, 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 {
    Thread.sleep(forTimeInterval: 5)
    return Date()
}

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())")
}

Now all we need to do is 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:

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())")
}

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. Any code between the async-let and the await, however, will execute concurrently with the takesTooLong() function.

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

Start 2022-03-30 14:18:40 +0000
After async-let 2022-03-30 14:18:40 +0000
result = 2022-03-30 14:18:45 +0000
End 2022-03-30 14:18:45 +0000

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 makes use of 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: TimeInterval) async throws {
    
    if delay < 5 {
        throw DurationError.tooShort
    } else if delay > 20 {
        throw DurationError.tooLong
    }
    
    Thread.sleep(forTimeInterval: delay)
    print("Async task completed at \(Date())")
}

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

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())")
}

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 +0000

Understanding Tasks

Any work that executes asynchronously is running 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. In terms of 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 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.

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 some extra cost in terms of having to do a little more work to create and manage tasks.

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

Task {
    await doSomething()
}

These tasks also inherit the configuration of the parent from which they are called, such as the actor context (a topic we will explore in the chapter entitled An Introduction to Swift Actors), priority, and task local variables. Tasks can also be assigned a new priority when they are created, for example:

Task(priority: .high) {
    await doSomething()
}

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:

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

Detached Tasks

Detached tasks are another form of unstructured concurrency, but 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:

Task.detached {
    await doSomething()
}

Detached tasks may also be passed a priority value, and checked for cancellation using the same techniques as outlined above:

let detachedTask = Task(priority: .medium) {
    await doSomething()
}
.
.          
if (!detachedTask.isCancelled) {
    detachedTask.cancel()
}

Task Management

Regardless of 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 task scope.

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

let detachedTask = Task(priority: .medium) {
    await doSomething()
}
.
.          
if (!detachedTask.isCancelled) {
    detachedTask.cancel()
}

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
}

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

do {
    try Task.checkCancellation()
} catch {
   // Perform task cleanup
}

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

Task.cancel()

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()

Working with Task Groups

So far in this chapter, all of our examples have involved creating one or two tasks (a parent and a child). In each case, we knew in advance of writing the code how many tasks were required. 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:

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

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 +0000

Note that the tasks all show the same completion timestamp indicating that they 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 keep in mind that there is no guarantee that tasks will complete in the order that 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. As an example, let’s assume that 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 takesTooLong()
            }
        }
    }
}

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

Mutation of captured var 'timeStamps' in concurrently-executing code

The problem here is that we have multiple tasks accessing the data concurrently 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. Actors, and how they might be used to solve this particular problem, will be covered in the chapter entitled An Introduction to Swift Actors.

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
.
.

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 takesTooLong())
            }
        }
    }
}

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.

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 takesTooLong())
            }
        }
        
        for await (task, date) in group {
            timeStamps[task] = date
        }
    }
}

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)")
    }
}

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

Task = 1, Date = 2022-03-31 17:48:20 +0000
Task = 5, Date = 2022-03-31 17:48:20 +0000
Task = 2, Date = 2022-03-31 17:48:20 +0000
Task = 4, Date = 2022-03-31 17:48:20 +0000
Task = 3, Date = 2022-03-31 17:48:20 +0000

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.

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

Summary

Modern CPUs and operating systems are designed to execute code concurrently allowing multiple tasks to be performed at the same time. This is achieved by running tasks on different threads with the main thread being 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. When working with concurrency, it is important to avoid data races, a problem that can usually be resolved using Swift Actors, a topic we will cover in the next chapter entitled An Introduction to Swift Actors.

Introduction to SwiftUI Essentials

The goal of this book is to teach the skills necessary to build iOS 15 applications using SwiftUI, Xcode 13, and the Swift 5.5 programming language.

Beginning with the basics, this book provides an outline of the steps necessary to set up an iOS development environment together with an introduction to the use of Swift Playgrounds to learn and experiment with Swift.

The book also includes in-depth chapters introducing the Swift 5.5 programming language including data types, control flow, functions, object-oriented programming, property wrappers, structured concurrency, and error handling.

An introduction to the key concepts of SwiftUI and project architecture is followed by a guided tour of Xcode in SwiftUI development mode. The book also covers the creation of custom SwiftUI views and explains how these views are combined to create user interface layouts including the use of stacks, frames, and forms.

Other topics covered include data handling using state properties in addition to observable, state, and environment objects, as are the key user interface design concepts such as modifiers, lists, tabbed views, context menus, user interface navigation, and outline groups.

The book also includes chapters covering graphics drawing, user interface animation, view transitions and gesture handling, WidgetKit, document-based apps, Core Data, CloudKit, and SiriKit integration.

Chapters are also provided explaining how to integrate SwiftUI views into existing UIKit-based projects and explains the integration of UIKit code into SwiftUI.

Finally, the book explains how to package up a completed app and upload it to the App Store for publication.

Along the way, the topics covered in the book are put into practice through detailed tutorials, the source code for which is also available for download.

The aim of this book, therefore, is to teach you the skills necessary to build your own apps for iOS 15 using SwiftUI. Assuming you are ready to download the iOS 15 SDK and Xcode 13 and have an Apple Mac system you are ready to get started.

For Swift Programmers

This book has been designed to address the needs of both existing Swift programmers and those who are new to both Swift and iOS app development. If you are familiar with the Swift 5.5 programming language, you can probably skip the Swift-specific chapters. If you are not yet familiar with the SwiftUI-specific language features of Swift, however, we recommend that you at least read the sections covering implicit returns from single expressions, opaque return types, and property wrappers. These features are central to the implementation and understanding of SwiftUI.

For Non-Swift Programmers

If you are new to programming in Swift then the entire book is appropriate for you. Just start at the beginning and keep going.

Source Code Download

The source code and Xcode project files for the examples contained in this book are available for download at:

https://www.ebookfrenzy.com/web/swiftui-ios15/

Errata

While we make every effort to ensure the accuracy of the content of this book, it is inevitable that a book covering a subject area of this size and complexity may include some errors and oversights. Any known issues with the book will be outlined, together with solutions at the following URL:

https://www.ebookfrenzy.com/errata/swiftui-ios15.html

In the event that you find an error not listed in the errata, please let us know by emailing our technical support team at [email protected].