Swift 异步编程允许任务并发运行而非顺序执行,从而提升应用性能并保持UI响应。本文介绍如何在 Swift 中使用 async let 并发运行后台任务。
前言
Async/await 语法于 Swift 5.5 引入,提供了一种更可读的异步代码编写方式,比调度队列和回调更易于理解。它与其他语言如 C# 或 JavaScript 的语法类似。async let 用于并行运行多个后台任务并等待它们的结果。
长期运行的任务阻塞了UI
在同步程序中,代码线性执行,当前任务完成前会阻塞后续任务。当长期运行的任务(如下载文件)同步执行时,UI 会变得无响应,直到任务完成,这导致糟糕的用户体验。
以下代码模拟同步下载文件,UI 在任务完成前无法响应:
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()
}
}
使用 async/await 在后台执行任务
将 ViewModel 的 downloadFile 方法改为异步,确保模型更新在 UI 线程上执行(使用 MainActor.run):
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()
}
}
这允许后台下载文件,同时 UI 保持响应并显示进度。
在后台执行多个任务
扩展以处理多个文件,ViewModel 持有一个 DataFile 数组:
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()
}
}
此方式顺序下载文件,每个文件完成后才开始下一个。
使用 "async let" 下载多个文件
使用 async let 并行下载多个文件,提升效率。它立即给变量赋值承诺,允许代码继续执行,然后等待所有承诺完成。
原异步代码:
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: 与先前类似,但标题改为 "Parallel" 以指示并行执行。
结论
在后台执行长期运行任务以保持UI响应至关重要。async/await 提供了清晰的异步任务执行机制。默认情况下,方法按顺序调用后台任务,但 async let 允许立即返回并并行执行多个任务,然后一起等待结果,从而提升并发性能。