[iOS] Swift でも async / await したい
こんにちは。きんくまです。
次のiOS 15からasync / awaitが実装されるとのことなので、やってみました。
環境
- Xcode 13.0 beta 5
- macOS Big Sur
注意)まだbeta版なので、仕様が変わってリリース時に動かなくなる可能性もあります
つくったもの
ボタンを押すと、非同期処理でクエリ文字列がカウントアップしていきます。
ViewModel
できたもの
actor ViewModel: ObservableObject { @MainActor @Published var message: String? var count: Int = 0 func fetchMessage(url: String) async -> String? { await Task.sleep(1 * 1000 * 1000 * 1000) count += 1 let result = url + "?count=\(count)" await MainActor.run { [weak self] in self?.message = result } return result } }
actor
actor ViewModel: ObservableObject {
classでもstructでもなく、actorになっています。
actorは参照型(Reference Type)の、マルチスレッド対応の型になります。
以下の記事に詳しく書いてあります。
参考)
Actors in Swift: how to use and prevent data races
@MainActor
@MainActor @Published var message: String?
今回はmessageというプロパティをUIで参照します。UIで参照するときは、メインスレッドでアクセスする必要があります。
そのためにMainActorの宣言をしました。
以下の記事に詳しく書いてあります。
参考)
MainActor usage in Swift explained to dispatch to the main thread
async
func fetchMessage(url: String) async -> String? {
非同期に結果を返すので、今回の本命のasyncキーワードを定義します。
余談
asyncの発音ですが、前からあってるのか不安だったのですが、調べたら「エィシンク」でした。
このサイト、プレゼンから該当キーワードの発音部分だけを次々みられるサイトでした。面白いです。
How to pronounce async in American English
タイマー
await Task.sleep(1 * 1000 * 1000 * 1000)
ここは非同期処理を擬似的に作るためにいれてあるタイマーです。
MainActor.run
await MainActor.run { [weak self] in self?.message = result }
@MainActorのキーワードのプロパティをメインスレッドから変更するための記述です。DispatchQueue.main.async ではなくてOKになったんですね。
実行スレッドの検証
実行スレッドがどうなるかテストしてみました。
func fetchMessage(url: String) async -> String? { print("dbg download start") await Task.sleep(1 * 1000 * 1000 * 1000) count += 1 let result = url + "?count=\(count)" print("1 -> \(Thread.isMainThread)") // false await MainActor.run { [weak self] in print("2 -> \(Thread.isMainThread)") //true self?.message = result } return result }
actorのメソッド内(1)はメインスレッドではないですが、その中でMainActor.runを入れると(2)、その中だけはメインスレッドになることがわかりました。
View
呼び出し側です。
struct ContentView: View { @StateObject var model = ViewModel() var body: some View { VStack { Text(model.message ?? "") .padding() Button("push me") { didTapButton() } } } func didTapButton() { Task { await model.fetchMessage(url: "https://apple.com") } } }
同期処理から非同期処理を呼ぶ
同期処理(synchronous method) から 非同期処理(asynchronous method)を呼ぶときは Task を使って呼びます。
あと、何度かすでにでてきていますが、asyncのメソッドを呼ぶときはawaitをつけます。
Task { await model.fetchMessage(url: "https://apple.com") }
逐次実行
awaitの処理が入っていると、非同期メソッドの結果が返るまでそこで実行が止まります。
なので、こんなふうに中身を変えます。
Task { // 1 let result1 = await model.fetchMessage(url: "https://apple.com") // 2 print("result1 \(result1)") // 3 let result2 = await model.fetchMessage(url: "https://apple.com") // 4 print("result2 \(result2)") }
そうすると、実行時は以下になります。
- result1の結果が返るのを待つ。このときresult2のfetchMessageはまだ実行されない
- result1の結果が返る。2が実行される
- result2の結果が返るのを待つ
- result2の結果が返る。4が実行される
この挙動によって、順番に非同期処理を実行したいときに、メソッドの入れ子が深くなる、いわゆる「死のピラミッド(Pyramid of doom)」を書かなくてもよくなるわけですね。
感想
非同期処理を同期処理から呼ぶところと、メインスレッド用のMainActorとかがわかってよかったです。
ではでは。