【SwiftUI】async/await

SwiftUI

Swift5.5(iOS15〜)でasync/awaitが追加されました。
async/awaitは非同期処理を行う際に使用するもので、
以前ではDispatchQueueを使っていたものを書き換える事ができます。

他の言語の非同期処理を行なっている場合は馴染みがあるかもしれません。

Swiftでasync/awaitを説明する時はActorも同時に説明される事が多いのですが、
今回は最小限にして分かりやすくする為に、まずはActor無しで説明します。

しかしActorを使用しないとasyncメソッドはViewに実装しないと非同期で動作しません。
一通り説明した後にActorを使用して説明します。
またActorについての詳細は別記事を用意する予定です。

async

asyncは非同期処理を行うメソッドに付けます。

func waitFunc() async -> String {
    sleep(5)
    return "Finish"
}

これは非同期で5秒待った後に文字列を返すメソッドです。
メソッドの引数の後、返り値の前にasyncを宣言します。

これでawaitを使用して非同期で実行できる様になります。
なお同期では実行できなくなるので、同期で実行したい処理はメソッド分けして、
個別で呼べる様にする必要があります。

await

awaitはasyncメソッドを非同期で実行する際に使用します。

let text = await waitFunc()
print(text)

メソッドの前にawaitをつける事で非同期で実行する事ができます。
メソッドは別スレッドで実行され、終了後に次の処理がメインスレッド実行されます。

その間メインスレッドは停止しない為、UIなどを停止することなく処理を行う事ができます。

Task

awaitをasyncメソッド外で呼ぶ場合はTaskで囲う必要があります。
Task外に書いた処理はawaitの待ちが行われずに実行されます。

Task{
    let text = await waitFunc()
    print("waitFunc後に実行")
}
print("waitFuncと並列実行")

実際の実行例

実際にSwiftUIで非同期処理を行なっていきます。
この例ではボタンを押した5秒後にテキストを変更します。

struct ContentView: View {
    
    @State var text = "Default"
    
    var body: some View {
        VStack {
            Text(self.text)
            Button("Wait"){
                self.text = "Waiting"
                Task{
                    self.text = await waitFunc()
                    print("waitFunc後に実行")
                }
                print("waitFuncと並列実行")
            }
        }
    }
    
    func waitFunc() async -> String {
        sleep(5)
        return "Finish"
    }
}

waitFuncはサブスレッドで実行される為、UI操作がブロッキングされません。
waitFunc実行後はメインスレッドに戻って来る為、
代入処理はメインスレッドで行われるので問題なくUIの更新処理も行えます。

DispatchQueueとの比較

struct ContentView: View {
    
    @State var text = "Default"
    
    var body: some View {
        VStack {
            Text(self.text)
            Button("Wait"){
                self.text = "Waiting"
                DispatchQueue.global().async {
                    let result = waitFunc()

                    DispatchQueue.main.async {
                        self.text = result
                        print("waitFunc後に実行")
                    }
                }
                print("waitFuncと並列実行")
            }
        }
    }
    
    func waitFunc() -> String {
        sleep(5)
        return "Finish"
    }
}

DispatchQueueを使うとこのようになります。

DispatchQueueはサブスレッドに処理を投げた後は投げっぱなしになります。
Viewの更新はメインスレッドで行わないといけない為、
処理が終わった後にサブスレッドからメインスレッドへViewの更新処理を投げ返します。

async/awaitでは非同期処理の実行後は元のスレッドに戻り処理を続行する為、
意識してスレッドを戻す必要なく、一連の流れとして処理を書くことができます。

例えば、非同期処理→Viewの更新→非同期処理→Viewの更新・・・と繰り返す場合、
毎回Viewの更新にDispatchQueueを書くのと、非同期処理の前にawaitを書くだけでは、
コードの分量や見通しやすさが大きく変わると思います。

Actor

分かりやすくする為にasync/awaitだけを抜き出して紹介しましたが、
asyncメソッドをView以外に書く場合はActorが無いと非同期で動作しません。

Actorとはスレッドセーフな処理を行う為の機能であり、
async/awaitと合わせて使う為の機能です。
今回は使い方のみを説明し、詳細な解説は別記事を書く予定です。

まずは先程例からasyncメソッドを別クラスに分離します。

struct ContentView: View {
    
    @State var text = "Default"
    
    var body: some View {
        VStack {
            Text(self.text)
            Button("Wait"){
                self.text = "Waiting"
                Task{
                    let asyncClass = AsyncClass()
                    self.text = await asyncClass.waitFunc()
                    print("waitFunc後に実行")
                }
                print("waitFuncと並列実行")
            }
        }
    }
}

class AsyncClass{
    func waitFunc() async -> String {
        sleep(5)
        return "Finish"
    }
}

単純にclassにasyncメソッドを分離するだけでは同期で実行されます。
この例を実行するとボタンを押した際にメインスレッドが停止し、
UIの操作が効かなくなります。

次にActorを導入して見ましょう。

actor AsyncClass{
    func waitFunc() async -> String {
        sleep(5)
        return "Finish"
    }
}

classをactorに書き換えました。
これで非同期で実装される様になります。

基本的にはasync/awaitとactorはセットで使用することになります。
ただし、この方法でactorを実装すると
クラス外からアクセスする場合は全て非同期で行わなければいけません。
メソッドだけでなくメンバ変数もです。

actor TestActor{
    var text = "Test"
}

func printMember(){
    Task{
        let asyncClass = AsyncClass()
        await print(asyncClass.text)
    }
}

こういった状況を回避する為に、部分的にActorを指定したり別なActorを指定する機能もあります。
例としては、@MainActorという属性が用意されています。
詳細について後日Actorについての記事で紹介します。

コメント

  1. […] […]

  2. […] […]

  3. […] […]

タイトルとURLをコピーしました