記事ディレクトリ
序文
async/await 構文は Swift 5.5 で導入され、WWDC 2021 の Swift アライメントにおける async/await の紹介で取り上げられました。これは非同期コードを記述するための読みやすい方法であり、ディスパッチ キューやコールバック関数よりも理解しやすいです。Async/await 構文は、C# や JavaScript などの他のプログラミング言語で使用される構文と似ています。「async let」の使用は、複数のバックグラウンド タスクを並行して実行し、それらの結合結果を待つことです。
Swift の非同期プログラミングは、特定のタスクを順次ではなく同時に実行できるようにするコードを記述する方法です。これにより、アプリケーションのパフォーマンスが向上し、複数のタスクを同時に実行できるようになりますが、さらに重要なのは、タスクがバックグラウンド スレッドで実行されている間にユーザー インターフェイスがユーザー入力に確実に応答できるようにするために使用できることです。
長時間実行されるタスクが UI をブロックしている
同期プログラムでは、コードは上から下へ線形に実行されます。プログラムは、現在のタスクが完了するのを待ってから、次のタスクに進みます。これにより、ユーザー インターフェイス (UI) の点で問題が発生します。長時間実行されるタスクが同期的に実行されると、プログラムがブロックされ、タスクが完了するまで UI が応答しなくなるからです。
以下のコードは、ファイルの同期ダウンロードなど、長時間実行されるタスクをシミュレートします。その結果、タスクが完了するまで UI が応答しなくなります。このようなユーザー エクスペリエンスは受け入れられません。
モデル:
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
}
}
}
ビューモデル:
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)
}
}
意見:
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
非同期になるように変更します。DataFile
モデルはビューによってリッスンされるため、モデルへの変更は UI スレッドで実行する必要があることに注意してください。これは、すべてのモデル更新をラップするMainActorキューを使用して行われますMainActor.run
。
ビューモデル
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)
}
}
意見:
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()
}
}
バックグラウンドで複数のタスクを実行する
バックグラウンドで 1 つのファイルがダウンロードされ、UI に進行状況が表示されたので、それを複数のファイルに変更しましょう。単一のファイルではなく配列ViewModel
を保持するように変更されました。すべてのファイルを反復処理して各ファイルをダウンロードするメソッドをDataFiles
追加します。downloadFiles
ビューはDataFiles
配列にバインドされ、各ファイルのダウンロードの進行状況を表示するように更新されます。ダウンロード ボタンは async にバインドされていますdownloadFiles
。
ビューモデル:
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)
]
}
}
意見:
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」を使用して複数のファイルの同時ダウンロードをシミュレートする
各タスクが他のタスクから独立しているため、上記のコードを改善して複数のダウンロードを並行して実行できます。Swift の同時実行性では、これはasync let
変数に値を即座に割り当てる Promise によって実現され、コードが次のコード行を実行できるようになります。次に、コードはそれらの Promise を待ち、最終結果が完了するまで待ちます。
非同期/待機:
func downloadFiles() async {
for index in files.indices {
let num = await downloadFile(index)
await MainActor.run {
fileCount += num
}
}
}
非同期させます
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
}
}
ビューモデル
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)
]
}
}
意見
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()
}
}
結論は
長時間実行されるタスクをバックグラウンドで実行し、UI の応答性を維持することが重要です。async/await は、非同期タスクを実行するためのクリーンなメカニズムを提供します。メソッドがバックグラウンドで複数のメソッドを呼び出すことがありますが、デフォルトでは、これらの呼び出しは順番に行われます。async を使用すると、すぐに返されるため、コードは次の呼び出しを行うことができ、返されたすべてのオブジェクトを一緒に待つことができます。これにより、複数のバックグラウンド タスクを並行して実行できるようになります。