この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは。きんくまです。
次の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とかがわかってよかったです。
ではでは。