[iOS] Swift でも async / await したい

2021.09.07

この記事は公開されてから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)")
        }

そうすると、実行時は以下になります。

  1. result1の結果が返るのを待つ。このときresult2のfetchMessageはまだ実行されない
  2. result1の結果が返る。2が実行される
  3. result2の結果が返るのを待つ
  4. result2の結果が返る。4が実行される

この挙動によって、順番に非同期処理を実行したいときに、メソッドの入れ子が深くなる、いわゆる「死のピラミッド(Pyramid of doom)」を書かなくてもよくなるわけですね。

感想

非同期処理を同期処理から呼ぶところと、メインスレッド用のMainActorとかがわかってよかったです。
ではでは。