この投稿は何?
Swift 5.6で導入された言語機能「async/await構文による非同期処理」について、シンプルなアプリ開発を通して学びます。
Swiftを基礎から学ぶには
自著、工学社より発売中の「まるごと分かるSwiftプログラミング」をお勧めします。変数、関数、フロー制御構文、データ構造はもちろん、構造体からクロージャ、エクステンション、プロトコル、クロージャまでを基礎からわかりやすく解説しています。
環境
- macOS 12.3.1
- Xcode 13.3.1
- Swift 5.6
非同期関数
非同期的に実行される関数を定義するには、宣言にasync
キーワードをマークします。
そして、ボディではコード実行を一時的に停止(サスペンド)する箇所を、await
キーワードでマークします。
func someFunction() async {
await anotherAsyncFunction()
}
await someFunction()
非同期関数の呼び出し時には、await
キーワードをマークする点に注目できます。
開発するアプリの概要
SwiftUIフレームワークを使って、「スライダーでテキストの不透明度を制御する」アプリを開発します。
非同期的な処理を理解する方法
開発するアプリではスライダーの他に、以下に挙げる2つのボタンを実装します。
- 同期的に5秒を数えるボタン
- 非同期的に5秒を数えるボタン
これらのボタンをタップしたときの挙動が、それぞれどのように異なるのかを体験することで「非同期的な処理とは何か」を理解します。
テキストの不透明度を制御する
まずは、テキストの不透明度を制御するスライダーを実装します。
テキストの不透明度はステート変数opacity
に保持します。
import SwiftUI
struct ContentView: View {
@State var opacity = 1.0 // 不透明度のステート変数
var body: some View {
VStack {
Text("Hello")
.font(.system(size: 65))
.fontWeight(.bold)
.opacity(opacity)
// テキストの不透明度を制御する
Slider(value: $opacity, in: 0...1) {
Text("Opacity")
}
}
}
}
ユーザーがスライダーを操作すると、ステート変数の値が変化します。
ステート変数の値が変化すると、SwiftUIは自動的に画面の描画を更新します。
同期的に5秒を数えるボタン
次に、「5秒を数えるボタン」の実装です。
sleep()
関数を使って、任意の秒数を数えることができます。
import SwiftUI
struct ContentView: View {
@State var opacity = 1.0
var body: some View {
VStack {
Text("Hello")
.font(.system(size: 65))
.fontWeight(.bold)
.opacity(opacity)
Slider(value: $opacity, in: 0...1) {
Text("Opacity")
}
// 同期的に5秒を数えるボタン
Button {
sleep(5)
} label: {
Text("Count")
.frame(maxWidth: 180, maxHeight: 66)
}
.buttonStyle(.bordered)
}
}
}
ユーザーがこのボタンをタップすると、5秒を数える間はアプリの画面が硬直します。
つまり、ボタンをタップしてから5秒間はスライダーを動かすことができません。
ただし、5秒を過ぎた時点で、スライダーはそれまでに操作された結果を反映します。
ユーザーはこのように同期的な挙動をするUIに対して、操作の遅延を感じてしまいます。
非同期的に5秒を数える
今度は、非同期的に5秒を数えるボタンの実装です。
同期的なsleep()
関数の代わりに、Task
型のsleep(nanoseconds:)
メソッドを使って5秒を数えます。
sleep(nanoseconds:)メソッドは非同期関数なので、呼び出す際に
awaitキーワードをマークする必要があります。 また、スロー関数でもあるので、
tryキーワードもマークします。(ここではエラーを無視するので疑問符
?`付きにします。)
注目すべき点としてTask{...}
ブロックがあります。
await
キーワードによる非同期関数の呼び出しは、非同期的なコンテキストにおいてのみ有効です。
通常の同期的なコンテキストで非同期関数を呼び出すことはできません。
そのため、Task{...}
ブロックで非同期的なコンテキストを作成します。
なお、ここでは5秒間のカウント完了を明確にするため、ステート変数isTapped
に基づいてテキストカラーを変更しています。
import SwiftUI
struct ContentView: View {
@State var opacity = 1.0
@State var isTapped = false // 非同期的なボタンが押されたかどうか
var body: some View {
VStack {
// 非同期ボタンが押されると、色が変わるテキスト
Text("Hello")
.foregroundColor(isTapped ? .red : .black)
.font(.system(size: 65))
.fontWeight(.bold)
.opacity(opacity)
Slider(value: $opacity, in: 0...1) {
Text("Opacity")
}
.padding()
// 同期的に5秒を数えるボタン
Button {
sleep(5)
} label: {
Text("Count")
.frame(maxWidth: 180, maxHeight: 66)
}
.buttonStyle(.bordered)
// 非同期的に5秒を数えるボタン
Button {
// 非同期的なタスクを作成する
Task {
// タスクの中は非同期的なコンテキストなので、
// 非同期関数を呼び出せる
try? await Task.sleep(nanoseconds: 5_000_000_000)
isTapped.toggle()
}
} label: {
Text("Async count")
.frame(maxWidth: 180, maxHeight: 66)
}
.buttonStyle(.bordered)
}
}
}
ユーザーがこの非同期的なボタンをタップしても、アプリは硬直せずにスライダーを操作し続けることができます。
もちろん、テキストの不透明度もスライダー操作に遅延することなく反応します。
そして、ボタンをタップしてから5秒すると、テキストカラーが変化します。
同期関数と非同期関数の違い
同期的な関数(いわゆる通常の関数)ではどんなに時間がかかろうとも、「その関数の実行が完了」するまで、コードは次の関数を実行できません。
そのため、5秒を数えている間、コードはUIの処理を実行できないので、スライダーは硬直しました。
対照的に非同期関数は、await
キーワードが示す「時間がかかる処理(ここでは5秒のカウント)」の箇所で一時停止して、その間に他のコードの実行をできます。
そのため、5秒を数える間もコードはUIの処理を実行できるので、スライダーはユーザーの操作に反応しました。