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