Use async let to run background tasks concurrently in Swift

foreword

The async/await syntax was introduced in Swift 5.5 and was covered in the Meet async/await in Swift alignment at WWDC 2021. It's a more readable way of writing asynchronous code, and easier to understand than dispatch queues and callback functions. Async/await syntax is similar to that used in other programming languages ​​such as C# or JavaScript. The use of "async let" is to run multiple background tasks in parallel and wait for their combined results.

Asynchronous programming in Swift is a way of writing code that allows certain tasks to run concurrently rather than sequentially. This can improve the performance of the application, allowing it to perform multiple tasks simultaneously, but more importantly, it can be used to ensure that the user interface is responsive to user input while tasks are performed on a background thread.

Long-running tasks are blocking the UI

In a synchronous program, code runs in a linear, top-to-bottom fashion. The program waits for the current task to complete before moving on to the next task. This creates problems in terms of user interface (UI), 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 code below simulates a long-running task, such as downloading a file synchronously, with the result that the UI becomes unresponsive until the task completes. Such user experience is unacceptable.

Model:

struct DataFile : Identifiable, Equatable {
    
    
    var id: Int
    var fileSize: Int
    var downloadedSize = 0
    var isDownloading = false
    
    init(id: Int, fileSize: Int) {
    
    
        self.id = id
        self.fileSize = fileSize
    }
    
    var progress: Double {
    
    
        return Double(self.downloadedSize) / Double(self.fileSize)
    }
    
    mutating func increment() {
    
    
        if downloadedSize < fileSize {
    
    
            downloadedSize += 1
        }
    }
}

ViewModel:

class DataFileViewModel: ObservableObject {
    
    
    @Published private(set) var file: DataFile
    
    init() {
    
    
        self.file = DataFile(id: 1, fileSize: 10)
    }
    
    func downloadFile() {
    
    
        file.isDownloading = true

        for _ in 0..<file.fileSize {
    
    
            file.increment()
            usleep(300000)
        }

        file.isDownloading = false
    }
    
    func reset() {
    
    
        self.file = DataFile(id: 1, fileSize: 10)
    }
}

View:

struct TestView1: View {
    
    
    @ObservedObject private var dataFiles: DataFileViewModel
    
    init() {
    
    
        dataFiles = DataFileViewModel()
    }
    
    var body: some View {
    
    
        VStack {
    
    
            /// 从文末源代码获取其实现
            TitleView(title: ["Synchronous"])
            
            Button("Download All") {
    
    
                dataFiles.downloadFile()
            }
            .buttonStyle(BlueButtonStyle())
            .disabled(dataFiles.file.isDownloading)
            
            HStack(spacing: 10) {
    
    
                Text("File 1:")
                ProgressView(value: dataFiles.file.progress)
                    .frame(width: 180)
                Text("\((dataFiles.file.progress * 100), specifier: "%0.0F")%")

                ZStack {
    
    
                    Color.clear
                        .frame(width: 30, height: 30)
                    if dataFiles.file.isDownloading {
    
    
                        ProgressView()
                            .progressViewStyle(CircularProgressViewStyle(tint: .blue))
                    }
                }
            }
            .padding()
            
            Spacer().frame(height: 200)

            Button("Reset") {
    
    
                dataFiles.reset()
            }
            .buttonStyle(BlueButtonStyle())

            Spacer()
        }
        .padding()
    }
}

Simulate a synchronous download of a file -- no real-time update UI

Executing tasks in the background using async/await

Modify the method in the ViewModel downloadFileto be asynchronous. Note that since DataFilethe model is listened to by the view, any changes to the model need to be performed on the UI thread. This is done by using the MainActorMainActor.run queue, which wraps all model updates.

ViewModel

class DataFileViewModel2: ObservableObject {
    
    
    @Published private(set) var file: DataFile
    
    init() {
    
    
        self.file = DataFile(id: 1, fileSize: 10)
    }
    
    func downloadFile() async -> Int {
    
    
        await MainActor.run {
    
    
            file.isDownloading = true
        }
        
        for _ in 0..<file.fileSize {
    
    
            await MainActor.run {
    
    
                file.increment()
            }
            usleep(300000)
        }
        
        await MainActor.run {
    
    
            file.isDownloading = false
        }
        
        return 1
    }
    
    func reset() {
    
    
        self.file = DataFile(id: 1, fileSize: 10)
    }
}

View:

struct TestView2: View {
    
    
    @ObservedObject private var dataFiles: DataFileViewModel2
    @State var fileCount = 0
    
    init() {
    
    
        dataFiles = DataFileViewModel2()
    }
    
    var body: some View {
    
    
        VStack {
    
    
            TitleView(title: ["Asynchronous"])
            
            Button("Download All") {
    
    
                Task {
    
    
                    let num = await dataFiles.downloadFile()
                    fileCount += num
                }
            }
            .buttonStyle(BlueButtonStyle())
            .disabled(dataFiles.file.isDownloading)
            
            Text("Files Downloaded: \(fileCount)")
            
            HStack(spacing: 10) {
    
    
                Text("File 1:")
                ProgressView(value: dataFiles.file.progress)
                    .frame(width: 180)
                Text("\((dataFiles.file.progress * 100), specifier: "%0.0F")%")
                
                ZStack {
    
    
                    Color.clear
                        .frame(width: 30, height: 30)
                    if dataFiles.file.isDownloading {
    
    
                        ProgressView()
                            .progressViewStyle(CircularProgressViewStyle(tint: .blue))
                    }
                }
            }
            .padding()
            
            Spacer().frame(height: 200)
            
            Button("Reset") {
    
    
                dataFiles.reset()
            }
            .buttonStyle(BlueButtonStyle())
            
            Spacer()
        }
        .padding()
    }
}

Use async/await to simulate downloading a file while updating the UI

Perform multiple tasks in the background

Now that we have one file downloading in the background and the UI shows progress, let's change it to multiple files. ViewModelwas changed to hold an DataFilesarray instead of a single file. Add a downloadFilesmethod to iterate over all files and download each one.

The view is bound to DataFilesthe array and updated to show the download progress of each file. The download button is bound to async downloadFiles.

ViewModel:

class DataFileViewModel3: ObservableObject {
    
    
    @Published private(set) var files: [DataFile]
    @Published private(set) var fileCount = 0
    
    init() {
    
    
        files = [
            DataFile(id: 1, fileSize: 10),
            DataFile(id: 2, fileSize: 20),
            DataFile(id: 3, fileSize: 5)
        ]
    }
    
    var isDownloading : Bool {
    
    
        files.filter {
    
     $0.isDownloading }.count > 0
    }
    
    func downloadFiles() async {
    
    
        for index in files.indices {
    
    
            let num = await downloadFile(index)
            await MainActor.run {
    
    
                fileCount += num
            }
        }
    }
    
    private func downloadFile(_ index: Array<DataFile>.Index) async -> Int {
    
    
        await MainActor.run {
    
    
            files[index].isDownloading = true
        }
        
        for _ in 0..<files[index].fileSize {
    
    
            await MainActor.run {
    
    
                files[index].increment()
            }
            usleep(300000)
        }
        await MainActor.run {
    
    
            files[index].isDownloading = false
        }
        return 1
    }
    
    func reset() {
    
    
        files = [
            DataFile(id: 1, fileSize: 10),
            DataFile(id: 2, fileSize: 20),
            DataFile(id: 3, fileSize: 5)
        ]
    }
}

View:

struct TestView3: View {
    
    
    @ObservedObject private var dataFiles: DataFileViewModel3
    
    init() {
    
    
        dataFiles = DataFileViewModel3()
    }
    
    var body: some View {
    
    
        VStack {
    
    
            TitleView(title: ["Asynchronous", "(multiple Files)"])
            
            Button("Download All") {
    
    
                Task {
    
    
                    await dataFiles.downloadFiles()
                }
            }
            .buttonStyle(BlueButtonStyle())
            .disabled(dataFiles.isDownloading)
            
            Text("Files Downloaded: \(dataFiles.fileCount)")
            
            ForEach(dataFiles.files) {
    
     file in
                HStack(spacing: 10) {
    
    
                    Text("File \(file.id):")
                    ProgressView(value: file.progress)
                        .frame(width: 180)
                    Text("\((file.progress * 100), specifier: "%0.0F")%")
                    
                    ZStack {
    
    
                        Color.clear
                            .frame(width: 30, height: 30)
                        if file.isDownloading {
    
    
                            ProgressView()
                                .progressViewStyle(CircularProgressViewStyle(tint: .blue))
                        }
                    }
                }
            }
            .padding()
            
            Spacer().frame(height: 150)
            
            Button("Reset") {
    
    
                dataFiles.reset()
            }
            .buttonStyle(BlueButtonStyle())
            
            Spacer()
        }
        .padding()
    }
}

Use async await to simulate sequential download of multiple files

Download multiple files using "async let"

Use "async let" to simulate concurrent download of multiple files

The code above can be improved to perform multiple downloads in parallel, since each task is independent of the others. In Swift concurrency, this is async letachieved with a promise that immediately assigns a value to a variable, allowing the code to execute the next line of code. The code then waits on those promises, waiting for the final result to complete.

async/await:

    func downloadFiles() async {
    
    
        for index in files.indices {
    
    
            let num = await downloadFile(index)
            await MainActor.run {
    
    
                fileCount += num
            }
        }
    }

async let

    func downloadFiles() async {
    
    
        async let num1 = await downloadFile(0)
        async let num2 = await downloadFile(1)
        async let num3 = await downloadFile(2)
        
        let (result1, result2, result3) = await (num1, num2, num3)
        await MainActor.run {
    
    
            fileCount = result1 + result2 + result3
        }
    }

ViewModel

class DataFileViewModel4: ObservableObject {
    
    
    @Published private(set) var files: [DataFile]
    @Published private(set) var fileCount = 0
    
    init() {
    
    
        files = [
            DataFile(id: 1, fileSize: 10),
            DataFile(id: 2, fileSize: 20),
            DataFile(id: 3, fileSize: 5)
        ]
    }
    
    var isDownloading : Bool {
    
    
        files.filter {
    
     $0.isDownloading }.count > 0
    }
    
    func downloadFiles() async {
    
    
        async let num1 = await downloadFile(0)
        async let num2 = await downloadFile(1)
        async let num3 = await downloadFile(2)
        
        let (result1, result2, result3) = await (num1, num2, num3)
        await MainActor.run {
    
    
            fileCount = result1 + result2 + result3
        }
    }
    
    private func downloadFile(_ index: Array<DataFile>.Index) async -> Int {
    
    
        await MainActor.run {
    
    
            files[index].isDownloading = true
        }
        
        for _ in 0..<files[index].fileSize {
    
    
            await MainActor.run {
    
    
                files[index].increment()
            }
            usleep(300000)
        }
        await MainActor.run {
    
    
            files[index].isDownloading = false
        }
        return 1
    }
    
    
    func reset() {
    
    
        files = [
            DataFile(id: 1, fileSize: 10),
            DataFile(id: 2, fileSize: 20),
            DataFile(id: 3, fileSize: 5)
        ]
    }
}

View

struct TestView4: View {
    
    
    @ObservedObject private var dataFiles: DataFileViewModel4
    
    init() {
    
    
        dataFiles = DataFileViewModel4()
    }
    
    var body: some View {
    
    
        VStack {
    
    
            TitleView(title: ["Parallel", "(multiple Files)"])
            
            Button("Download All") {
    
    
                Task {
    
    
                    await dataFiles.downloadFiles()
                }
            }
            .buttonStyle(BlueButtonStyle())
            .disabled(dataFiles.isDownloading)
            
            Text("Files Downloaded: \(dataFiles.fileCount)")
            
            ForEach(dataFiles.files) {
    
     file in
                HStack(spacing: 10) {
    
    
                    Text("File \(file.id):")
                    ProgressView(value: file.progress)
                        .frame(width: 180)
                    Text("\((file.progress * 100), specifier: "%0.0F")%")
                    
                    ZStack {
    
    
                        Color.clear
                            .frame(width: 30, height: 30)
                        if file.isDownloading {
    
    
                            ProgressView()
                                .progressViewStyle(CircularProgressViewStyle(tint: .blue))
                        }
                    }
                }
            }
            .padding()
            
            Spacer().frame(height: 150)
            
            Button("Reset") {
    
    
                dataFiles.reset()
            }
            .buttonStyle(BlueButtonStyle())
            
            Spacer()
        }
        .padding()
    }
}

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

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

in conclusion

It is important to perform long-running tasks in the background and keep the UI responsive. async/await provides a clean mechanism to execute asynchronous tasks. Sometimes a method calls multiple methods in the background, and by default these calls are made sequentially. async makes it return immediately, allowing the code to make the next call, and then all returned objects can be awaited together. This enables multiple background tasks to run in parallel.

Guess you like

Origin blog.csdn.net/qq_36478920/article/details/131371849