Use async let to run background tasks in parallel in Swift

Use async let to run background tasks in parallel in Swift

The async/await syntax was introduced in Swift 5.5 with Meet async/await in Swift in WWDC 2021. It is a more readable way to write asynchronous code and is easier to understand than dispatch queues and callback functions. The async/await syntax is similar to those used in other programming languages like C# or JavaScript. Use of "async let" is used to run multiple background tasks in parallel and wait for their combined results.

Swift Asynchronous Programming is a method of writing code that allows certain tasks to run concurrently, rather than sequentially. This can improve performance of an app by allowing it to perform multiple tasks at the same time, but more importantly, it can be used to ensure the UI is responsive to user input while tasks are performed on background threads.



Long running activity blocks the UI

In a synchronous program, the code runs in a linear, top-to-bottom fashion. The program waits for the current line to complete before moving on to the next one. This can cause problems in a user interface (UI) context, because if a long-running task is executed synchronously, the program will block and the UI will become unresponsive until the task is completed.

The following code simulates a long running task such as downloading a file in a synchronous way with the result that thu UI becomes unresponsive until the task is complete. This demonstrates an unacceptable user experience as the starting point.


Model

 1struct DataFile : Identifiable, Equatable {
 2    var id: Int
 3    var fileSize: Int
 4    var downloadedSize = 0
 5    var isDownloading = false
 6    
 7    init(id: Int, fileSize: Int) {
 8        self.id = id
 9        self.fileSize = fileSize
10    }
11    
12    var progress: Double {
13        return Double(self.downloadedSize) / Double(self.fileSize)
14    }
15    
16    mutating func increment() {
17        if downloadedSize < fileSize {
18            downloadedSize += 1
19        }
20    }
21}

ViewModel

 1class DataFileViewModel: ObservableObject {
 2    @Published private(set) var file: DataFile
 3    
 4    init() {
 5        self.file = DataFile(id: 1, fileSize: 10)
 6    }
 7    
 8    func downloadFile() {
 9        file.isDownloading = true
10
11        for _ in 0..<file.fileSize {
12            file.increment()
13            usleep(300000)
14        }
15
16        file.isDownloading = false
17    }
18    
19    func reset() {
20        self.file = DataFile(id: 1, fileSize: 10)
21    }
22}

View

 1struct TestView1: View {
 2    @ObservedObject private var dataFiles: DataFileViewModel
 3    
 4    init() {
 5        dataFiles = DataFileViewModel()
 6    }
 7    
 8    var body: some View {
 9        VStack {
10            TitleView(title: ["Synchronous"])
11            
12            Button("Download All") {
13                dataFiles.downloadFile()
14            }
15            .buttonStyle(BlueButtonStyle())
16            .disabled(dataFiles.file.isDownloading)
17            
18            HStack(spacing: 10) {
19                Text("File 1:")
20                ProgressView(value: dataFiles.file.progress)
21                    .frame(width: 180)
22                Text("\((dataFiles.file.progress * 100), specifier: "%0.0F")%")
23
24                ZStack {
25                    Color.clear
26                        .frame(width: 30, height: 30)
27                    if dataFiles.file.isDownloading {
28                        ProgressView()
29                            .progressViewStyle(CircularProgressViewStyle(tint: .blue))
30                    }
31                }
32            }
33            .padding()
34            
35            Spacer().frame(height: 200)
36
37            Button("Reset") {
38                dataFiles.reset()
39            }
40            .buttonStyle(BlueButtonStyle())
41
42            Spacer()
43        }
44        .padding()
45    }
46}

Simulated downloading a file synchronously - no UI update



Use async/await to perform task in background

The code in the ViewModel is changed tom make the downloadFile method asynchronous. Note that as the DataFile model is being observed by the view, any changes to the model need to be performed on the UI thread. This is done using MainActor queue by wrapping any model updates with MainActor.run.

The view is updated to wrap the asynchronous call to downloadFile in a Task. This runs the download in the background while the UI remains responsive and displays progress.


ViewModel

 1class DataFileViewModel2: ObservableObject {
 2    @Published private(set) var file: DataFile
 3    
 4    init() {
 5        self.file = DataFile(id: 1, fileSize: 10)
 6    }
 7    
 8    func downloadFile() async -> Int {
 9        await MainActor.run {
10            file.isDownloading = true
11        }
12        
13        for _ in 0..<file.fileSize {
14            await MainActor.run {
15                file.increment()
16            }
17            usleep(300000)
18        }
19        
20        await MainActor.run {
21            file.isDownloading = false
22        }
23        
24        return 1
25    }
26    
27    func reset() {
28        self.file = DataFile(id: 1, fileSize: 10)
29    }
30}

View

 1struct TestView2: View {
 2    @ObservedObject private var dataFiles: DataFileViewModel2
 3    @State var fileCount = 0
 4    
 5    init() {
 6        dataFiles = DataFileViewModel2()
 7    }
 8    
 9    var body: some View {
10        VStack {
11            TitleView(title: ["Asynchronous"])
12            
13            Button("Download All") {
14                Task {
15                    let num = await dataFiles.downloadFile()
16                    fileCount += num
17                }
18            }
19            .buttonStyle(BlueButtonStyle())
20            .disabled(dataFiles.file.isDownloading)
21            
22            Text("Files Downloaded: \(fileCount)")
23            
24            HStack(spacing: 10) {
25                Text("File 1:")
26                ProgressView(value: dataFiles.file.progress)
27                    .frame(width: 180)
28                Text("\((dataFiles.file.progress * 100), specifier: "%0.0F")%")
29                
30                ZStack {
31                    Color.clear
32                        .frame(width: 30, height: 30)
33                    if dataFiles.file.isDownloading {
34                        ProgressView()
35                            .progressViewStyle(CircularProgressViewStyle(tint: .blue))
36                    }
37                }
38            }
39            .padding()
40            
41            Spacer().frame(height: 200)
42            
43            Button("Reset") {
44                dataFiles.reset()
45            }
46            .buttonStyle(BlueButtonStyle())
47            
48            Spacer()
49        }
50        .padding()
51    }
52}

Use async await to simulate downloading a file while UI is updated



Performing multiple tasks in the background

Now that we have one file downloading in the background and the UI displaying progress, let's change it to multiple files. The ViewModel is changed to hold an array of DataFiles rather than a single one. A downloadFiles method is added to loop through all the files and download each one.

The view is updated to bind to the array of DataFiles and display the download progress for each one. The Download button is bound to the async downloadFiles inside a Task.


ViewModel

 1class DataFileViewModel3: ObservableObject {
 2    @Published private(set) var files: [DataFile]
 3    @Published private(set) var fileCount = 0
 4    
 5    init() {
 6        files = [
 7            DataFile(id: 1, fileSize: 10),
 8            DataFile(id: 2, fileSize: 20),
 9            DataFile(id: 3, fileSize: 5)
10        ]
11    }
12    
13    var isDownloading : Bool {
14        files.filter { $0.isDownloading }.count > 0
15    }
16    
17    func downloadFiles() async {
18        for index in files.indices {
19            let num = await downloadFile(index)
20            await MainActor.run {
21                fileCount += num
22            }
23        }
24    }
25    
26    private func downloadFile(_ index: Array<DataFile>.Index) async -> Int {
27        await MainActor.run {
28            files[index].isDownloading = true
29        }
30        
31        for _ in 0..<files[index].fileSize {
32            await MainActor.run {
33                files[index].increment()
34            }
35            usleep(300000)
36        }
37        await MainActor.run {
38            files[index].isDownloading = false
39        }
40        return 1
41    }
42    
43    func reset() {
44        files = [
45            DataFile(id: 1, fileSize: 10),
46            DataFile(id: 2, fileSize: 20),
47            DataFile(id: 3, fileSize: 5)
48        ]
49    }
50}

View

 1struct TestView3: View {
 2    @ObservedObject private var dataFiles: DataFileViewModel3
 3    
 4    init() {
 5        dataFiles = DataFileViewModel3()
 6    }
 7    
 8    var body: some View {
 9        VStack {
10            TitleView(title: ["Asynchronous", "(multiple Files)"])
11            
12            Button("Download All") {
13                Task {
14                    await dataFiles.downloadFiles()
15                }
16            }
17            .buttonStyle(BlueButtonStyle())
18            .disabled(dataFiles.isDownloading)
19            
20            Text("Files Downloaded: \(dataFiles.fileCount)")
21            
22            ForEach(dataFiles.files) { file in
23                HStack(spacing: 10) {
24                    Text("File \(file.id):")
25                    ProgressView(value: file.progress)
26                        .frame(width: 180)
27                    Text("\((file.progress * 100), specifier: "%0.0F")%")
28                    
29                    ZStack {
30                        Color.clear
31                            .frame(width: 30, height: 30)
32                        if file.isDownloading {
33                            ProgressView()
34                                .progressViewStyle(CircularProgressViewStyle(tint: .blue))
35                        }
36                    }
37                }
38            }
39            .padding()
40            
41            Spacer().frame(height: 150)
42            
43            Button("Reset") {
44                dataFiles.reset()
45            }
46            .buttonStyle(BlueButtonStyle())
47            
48            Spacer()
49        }
50        .padding()
51    }
52}

Use async await to simulate downloading multiple files sequentially



Use "async let" to simulate downloading multiple files in parallel

The code above can be improved to perform the multiple downloads in parallel as each task is independent of the other tasks. In Swift concurrency this is achieved using async let, which sets a variable immediately with a promise, allowing the code to execute the next line of code. The code then awaits these promises for the final result to be complete.


async/await

1    func downloadFiles() async {
2        for index in files.indices {
3            let num = await downloadFile(index)
4            await MainActor.run {
5                fileCount += num
6            }
7        }
8    }

async let

 1    func downloadFiles() async {
 2        async let num1 = await downloadFile(0)
 3        async let num2 = await downloadFile(1)
 4        async let num3 = await downloadFile(2)
 5        
 6        let (result1, result2, result3) = await (num1, num2, num3)
 7        await MainActor.run {
 8            fileCount = result1 + result2 + result3
 9        }
10    }

ViewModel

 1class DataFileViewModel4: ObservableObject {
 2    @Published private(set) var files: [DataFile]
 3    @Published private(set) var fileCount = 0
 4    
 5    init() {
 6        files = [
 7            DataFile(id: 1, fileSize: 10),
 8            DataFile(id: 2, fileSize: 20),
 9            DataFile(id: 3, fileSize: 5)
10        ]
11    }
12    
13    var isDownloading : Bool {
14        files.filter { $0.isDownloading }.count > 0
15    }
16    
17    func downloadFiles() async {
18        async let num1 = await downloadFile(0)
19        async let num2 = await downloadFile(1)
20        async let num3 = await downloadFile(2)
21        
22        let (result1, result2, result3) = await (num1, num2, num3)
23        await MainActor.run {
24            fileCount = result1 + result2 + result3
25        }
26    }
27    
28    private func downloadFile(_ index: Array<DataFile>.Index) async -> Int {
29        await MainActor.run {
30            files[index].isDownloading = true
31        }
32        
33        for _ in 0..<files[index].fileSize {
34            await MainActor.run {
35                files[index].increment()
36            }
37            usleep(300000)
38        }
39        await MainActor.run {
40            files[index].isDownloading = false
41        }
42        return 1
43    }
44    
45    
46    func reset() {
47        files = [
48            DataFile(id: 1, fileSize: 10),
49            DataFile(id: 2, fileSize: 20),
50            DataFile(id: 3, fileSize: 5)
51        ]
52    }
53}

View

 1struct TestView4: View {
 2    @ObservedObject private var dataFiles: DataFileViewModel4
 3    
 4    init() {
 5        dataFiles = DataFileViewModel4()
 6    }
 7    
 8    var body: some View {
 9        VStack {
10            TitleView(title: ["Parallel", "(multiple Files)"])
11            
12            Button("Download All") {
13                Task {
14                    await dataFiles.downloadFiles()
15                }
16            }
17            .buttonStyle(BlueButtonStyle())
18            .disabled(dataFiles.isDownloading)
19            
20            Text("Files Downloaded: \(dataFiles.fileCount)")
21            
22            ForEach(dataFiles.files) { file in
23                HStack(spacing: 10) {
24                    Text("File \(file.id):")
25                    ProgressView(value: file.progress)
26                        .frame(width: 180)
27                    Text("\((file.progress * 100), specifier: "%0.0F")%")
28                    
29                    ZStack {
30                        Color.clear
31                            .frame(width: 30, height: 30)
32                        if file.isDownloading {
33                            ProgressView()
34                                .progressViewStyle(CircularProgressViewStyle(tint: .blue))
35                        }
36                    }
37                }
38            }
39            .padding()
40            
41            Spacer().frame(height: 150)
42            
43            Button("Reset") {
44                dataFiles.reset()
45            }
46            .buttonStyle(BlueButtonStyle())
47            
48            Spacer()
49        }
50        .padding()
51    }
52}

Use "async let" to simulate downloading multiple files in parallel




Use "async let" to simulate downloading multiple files in parallel




Conclusion

It is important to perform long-running tasks in the background and keep the UI responsive. async/await provides a clean mechanism to perform asynchronous tasks. There are times when a method is calling multiple methods in the background and the default is to make these calls in sequence. async let returns immediately allowing the code to proceed to the next call and then all the returned objects can be awaited together. This allows multiple background tasks to be performed in parallel.

Source code for AsyncLetApp is available on GitHub.