前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >如何在 Swift 中取消一个后台任务

如何在 Swift 中取消一个后台任务

作者头像
韦弦zhy
发布2023-03-12 10:45:59
2.7K0
发布2023-03-12 10:45:59
举报

Swift 5.5中引入的 async/await 语法,允许用更可读的方式来编写异步代码。异步编程可以提高应用程序的性能,但必须取消不需要的任务,以确保不需要的后台任务不会干扰到应用程序。本文演示了如何明确地取消一个任务,并展示了子任务是如何自动取消的。

该代码建立在在 Swift 中使用 async let 并行的运行后台任务中编写的AsyncLetApp之上。

为什么要取消一个后台任务

与视图的交互可能会触发后台任务的运行,进一步的交互可能会使最初的请求过时,并触发后续的后台任务运行。除了浪费资源外,不取消初始任务可能会导致你的应用程序出现偶现和意外行为。

一个取消按钮被添加到视图中,其点击事件是在ViewModel中调用取消方法。在ViewModel中添加了一些日志记录,以便在文件下载增加时和文件isDownloading属性被设置为false时打印出来。可以看到,在下载被取消后,任务继续进行,并最终将isDownloading属性设置为false。如果一个下载被取消,而随后的下载又迅速开始,这可能会在用户界面上造成问题———第一个任务的isDownloading属性被设置为false,效果是停止了第二次下载。

View:

代码语言:javascript
复制
struct NaiveCancelView: View {
    @ObservedObject private var dataFiles: DataFileViewModel5
    @State var fileCount = 0
    
    init() {
        dataFiles = DataFileViewModel5()
    }
    
    var body: some View {
        ScrollView {
            VStack {
                TitleView(title: ["Naive Cancel"])
                
                HStack {
                    Button("Download All") {
                        Task {
                            let num = await dataFiles.downloadFile()
                            fileCount += num
                        }
                    }
                    .buttonStyle(BlueButtonStyle())
                .disabled(dataFiles.file.isDownloading)
                    Button("Cancel") {
                        dataFiles.cancel()
                    }
                    .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()

                .onDisappear {
                    dataFiles.cancel()
                }
                
                Spacer().frame(height: 200)
                
                Button("Reset") {
                    dataFiles.reset()
                }
                .buttonStyle(BlueButtonStyle())
                
                Spacer()
            }
            .padding()
        }
    }
}

ViewModel:

代码语言:javascript
复制
class DataFileViewModel5: 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 {
                guard file.isDownloading else { return }
                file.increment()
                print("  --   Downloading file - \(file.progress * 100) %")
            }
            usleep(300000)
        }
        
        await MainActor.run {
            file.isDownloading = false
            print(" ***** File : \(file.id) setting isDownloading to FALSE")
        }
        
        return 1
    }
    
    func cancel() {
        file.isDownloading = false
        self.file = DataFile(id: 1, fileSize: 10)
    }
        
    func reset() {
        self.file = DataFile(id: 1, fileSize: 10)
    }
}

第二个下载任务因第一个后台任务的完成而停止

使用取消标志

有多种方法可以取消后台任务中的工作。一种机制是向具有异步任务的对象添加状态标志,并在任务运行时监视此标志。不需要对 View 进行任何更改,取消按钮仍然调用 ViewModel 中的 cancel 函数。

对 ViewModel 的更改包括添加一个 cancelFlag 布尔属性,该属性必须用 MainActor 标记,因为它需要在主 UI 线程上更新。模拟文件下载的循环根据两个条件从 for 循环更新为 while 循环:

  • 取消标志的值是 false
  • 文件正在下载

这解决了这个问题,但是有一个额外的标志来取消下载似乎太多余了。

View:

代码语言:javascript
复制
struct CancelFlagView: View {
    @ObservedObject private var dataFiles: DataFileViewModel6
    @State var fileCount = 0

    init() {
        dataFiles = DataFileViewModel6()
    }
    
    var body: some View {
        ScrollView {
            VStack {
                TitleView(title: ["Use Cancel Flag"])
                
                HStack {
                    Button("Download All") {
                        Task {
                            let num = await dataFiles.downloadFile()
                            fileCount += num
                        }
                    }
                    .buttonStyle(BlueButtonStyle())
                .disabled(dataFiles.file.isDownloading)
                    Button("Cancel") {
                        dataFiles.cancel()
                    }
                    .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()
                
                .onDisappear {
                    dataFiles.cancel()
                }

                Spacer().frame(height: 200)
                
                Button("Reset") {
                    dataFiles.reset()
                }
                .buttonStyle(BlueButtonStyle())
                
                Spacer()
            }
            .padding()
        }
    }
}

ViewModel:

代码语言:javascript
复制
class DataFileViewModel6: ObservableObject {
    @Published private(set) var file: DataFile
    
    @MainActor var cancelFlag = false
    
    init() {
        self.file = DataFile(id: 1, fileSize: 10)
    }
    
    func downloadFile() async -> Int {
        await MainActor.run {
            file.isDownloading = true
        }
        
        while await !cancelFlag, file.isDownloading {
            await MainActor.run {
                print("  --   Downloading file - \(file.progress * 100) %")
                file.increment()
            }
            usleep(300000)
        }
        
        await MainActor.run {
            file.isDownloading = false
            print(" ***** File : \(file.id) setting isDownloading to FALSE")
        }
        
        return 1
    }
  
    @MainActor
    func cancel() {
        self.cancelFlag = true
        print(" ***** File : \(file.id) setting cancelFlag to TRUE")
        self.reset()
    }
        
    @MainActor
    func reset() {
        self.file = DataFile(id: 1, fileSize: 10)
        self.cancelFlag = false
    }
}

在 ViewModel 中使用取消标志来结束后台循环

取消任务实例 - Task.checkCancellation()

一个更优雅的解决方案是为 Task 创建一个状态属性,并在下载按钮操作的视图中将任务分配给该属性。取消按钮可以取消这个任务。听起来很简单,对吧!好吧,还有一些事情要做,因为正如文档所说:

Tasks include a shared mechanism for indicating cancellation, but not a shared implementation for how to handle cancellation. 任务包括一个用于表示取消的共享机制,但是没有一个关于如何处理取消的共享实现。

这是因为任务的取消方式会因任务正在执行的操作而异。

在此示例中,ViewModel 中的 downloadFile 函数更改为在下载循环中使用 checkCancellation。这将检查是否取消,如果任务已被取消,则会抛出错误。抛出此错误时,可以将 isDownloading 标志设置为 false,并且可以选择重置 ViewModel。

这次,取消标志和所有相关代码都可以从 ViewModel 中完全删除。

View:

代码语言:javascript
复制
struct CancelTaskView: View {
    @ObservedObject private var dataFiles: DataFileViewModel7
    @State var fileCount = 0
    
    @State var fileDownloadTask: Task<Void, Error>?
    
    init() {
        dataFiles = DataFileViewModel7()
    }
    
    var body: some View {
        ScrollView {
            VStack {
                TitleView(title: ["Cancel Task", "", "<checkCancellation()>"])
                
                HStack {
                    Button("Download All") {
                        fileDownloadTask = Task {
                            let num = await dataFiles.downloadFile()
                            fileCount += num
                        }
                    }
                    .buttonStyle(BlueButtonStyle())
                    .disabled(dataFiles.file.isDownloading)
                    Button("Cancel") {
                        fileDownloadTask?.cancel()
                    }
                    .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()
                
                .onDisappear {
                    fileDownloadTask?.cancel()
                    dataFiles.reset()
                }
                
                Spacer().frame(height: 200)
                
                Button("Reset") {
                    fileDownloadTask?.cancel()
                    dataFiles.reset()
                }
                .buttonStyle(BlueButtonStyle())
                
                Spacer()
            }
            .padding()
        }
    }
}

ViewModel:

代码语言:javascript
复制
class DataFileViewModel7: 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
        }
        
        while file.isDownloading {
            do {
                try await MainActor.run {
                    try Task.checkCancellation()
                    
                    print("  --   Downloading file - \(file.progress * 100) %")

                    file.increment()
                }
                usleep(300000)
            } catch {
                print(" *************** Catch - The task has been cancelled")
                // Set the isDownloading flag to false
                await MainActor.run {
                    file.isDownloading = false
                    // reset()
                }
            }
        }
        
        await MainActor.run {
            file.isDownloading = false
            print(" ***** File : \(file.id) setting isDownloading to FALSE")
        }
        
        return 1
    }
        
    @MainActor
    func reset() {
        self.file = DataFile(id: 1, fileSize: 10)
    }
}

在 SwiftUI 中取消任务实例

任务取消传播到子任务 - Task.isCancelled

使用 checkCancellation 引发异常的代替方法是使用 isCancelled 查看任务是否已取消。

此方法仍然使用Task的状态属性。它被分配给下载按钮中的 downloadFiles 函数,任务通过视图中的取消按钮取消。

View:

代码语言:javascript
复制
struct CancelTaskMultipleView: View {
    @ObservedObject private var dataFiles: DataFileViewModel8
    
    @State var fileDownloadTask: Task<Void, Error>?
    
    init() {
        dataFiles = DataFileViewModel8()
    }
    
    var body: some View {
        ScrollView {
            VStack {
                TitleView(title: ["Cancel Task", "", "<Task.isCancelled>"])
                
                HStack {
                    Button("Download All") {
                        fileDownloadTask = Task {
                            await dataFiles.downloadFiles()
                        }
                    }
                    .buttonStyle(BlueButtonStyle())
                    .disabled(dataFiles.isDownloading)
                    
                    Button("Cancel") {
                        fileDownloadTask?.cancel()
                    }
                    .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()
                
                .onDisappear {
                    fileDownloadTask?.cancel()
                    dataFiles.reset()
                }
                
                Spacer().frame(height: 150)
                
                Button("Reset") {
                    fileDownloadTask?.cancel()
                    dataFiles.reset()
                }
                .buttonStyle(BlueButtonStyle())
                
                Spacer()
            }
            .padding()
        }
    }
}

ViewModel:

代码语言:javascript
复制
class DataFileViewModel8: 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
        }
        
        while files[index].isDownloading, !Task.isCancelled {
            await MainActor.run {
                print("  --   Downloading file \(files[index].id) - \(files[index].progress * 100) %")
                files[index].increment()
            }
            usleep(300000)
        }
        
        await MainActor.run {
            files[index].isDownloading = false
            print(" ***** File : \(files[index].id) setting isDownloading to FALSE")
        }
        return 1
    }
    
    @MainActor
    func reset() {
        files = [
            DataFile(id: 1, fileSize: 10),
            DataFile(id: 2, fileSize: 20),
            DataFile(id: 3, fileSize: 5)
        ]
    }
}

取消任务实例会取消 SwiftUI 中的子任务

在 SwiftUI 中取消和恢复后台任务

结论

在异步编程中,重要的是停止任何不需要的后台任务以节省资源并避免后台任务干扰应用程序的任何不良副作用。 Swift Async 框架提供了多种方式来表示任务已被取消,但是任务中的代码的实现者在任务被取消时做出适当的响应取决于。任务一旦被取消,就无法取消。检查任务是否已被取消的一种方法是使用 checkCancellation,这将引发错误。另一种是简单地使用 isCancelled 作为布尔标志来查看任务是否已被取消。

在异步编程中,必须停止任何不需要的后台任务,以节省资源,并避免后台任务干扰App带来的任何不必要的副作用。Swift异步框架提供了许多方法来表明任务已被取消,但这取决于任务中的代码实现者在任务被取消时做出适当的反应。一旦一个任务被取消,就不能再取消了。检查一个任务是否被取消的一种方法是使用checkCancellation,这将抛出一个错误。另一种方法是简单地使用isCancelled作为一个布尔标志来查看任务是否已经被取消。

GitHub 上提供了 AsyncLetApp 的源代码。

译自 https://swdevnotes.com/swift/2023/how-to-cancel-a-background-task-in-swift/

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-03-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么要取消一个后台任务
  • 使用取消标志
  • 取消任务实例 - Task.checkCancellation()
  • 任务取消传播到子任务 - Task.isCancelled
  • 结论
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档