【UIKit】async/await

以前SwiftUIでasync/awaitの記事を書きましたが、折角なのでUIKit版も書きました。
今回はStoryboard無しでAutoLayoutでやってます。

特に技術的な理由は無く私の好みの問題です。
普段SwiftUIばかりなのでコードで指定できるAutoLayoutの方がやりやすいからです。

なお概要からTaskの説明まではSwiftUI版と同様です。
サンプルコードの中身はテキストの変更からカウントアップに変更しています。

SwiftUI版はこちら

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

概要

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と並列実行")

サンプルコード

ここから UIKit版です。
ボタンを押すと5秒後にカウントアップするだけの簡単なサンプルです。
長いですが半分以上は UIの描画周りなのでviewDidLoadより下を見てください。

import UIKit

class ActorTestViewController:UIViewController {
    
    lazy var label:UILabel = {
        let label = UILabel()
        label.text = String(self.count)
        label.translatesAutoresizingMaskIntoConstraints = false
        
        return label
    }()
    
    let syncButton:UIButton = {
        let button = UIButton()
        button.backgroundColor = .blue
        button.setTitle("CountUp(sync)", for: .normal)
        button.addTarget(self, action: #selector(pushSyncButton(_:)), for: .touchDown)
        button.translatesAutoresizingMaskIntoConstraints = false
        
        return button
    }()
    
    let asyncButton:UIButton = {
        let button = UIButton()
        button.backgroundColor = .blue
        button.setTitle("CountUp(async)", for: .normal)
        button.addTarget(self, action: #selector(pushAsyncButton(_:)), for: .touchDown)
        button.translatesAutoresizingMaskIntoConstraints = false
        
        return button
    }()
    
    var count = 0
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.view.backgroundColor = .white
        
        self.view.addSubview(label)
        self.view.addSubview(syncButton)
        self.view.addSubview(asyncButton)
        
        NSLayoutConstraint.activate([
            label.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 0),
            label.centerXAnchor.constraint(equalTo: view.centerXAnchor, constant: 0),
            syncButton.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 10),
            syncButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            asyncButton.topAnchor.constraint(equalTo: syncButton.bottomAnchor, constant: 10),
            asyncButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
        ])
    }
    
    @objc func pushSyncButton(_ sender: UIButton) {
        print("Push sync")
        
        self.label.text = syncWaitCountUp()
    }
    
    func syncWaitCountUp() -> String{
        sleep(5)
        self.count += 1
        return String(self.count)
    }
    
    @objc func pushAsyncButton(_ sender: UIButton) {
        print("Push async")
        
        Task{
//            self.label.text = await asyncWaitCountUp() //これだとダメみたい
            let text = await asyncWaitCountUp()
            self.label.text = text
            print("asyncWaitCountUp後に実行")
        }
        print("asyncWaitCountUpと並列実行")
    }
    
    func asyncWaitCountUp() async -> String {
        sleep(5)
        self.count += 1
        return String(self.count)
    }
}

まずsyncWaitCountUpです。これは通常通り同期実行されます。
メインスレッドがブロッキングされる為UI操作が出来なくなります。

asyncWaitCountUpは非同期実行で、sleepはサブスレッドで処理される為
メインスレッドはブロッキングされずUIは自由に操作できるまま5秒待機します。
またawait後はメインスレッドに戻るので、 描画処理も問題なく処理されます。

余談ですが、awaitから直接 UIのメンバ変数へ代入するとブロッキングされる様です。
SwiftUIだとこれに近い書き方が許容されるのでSwiftUI感覚で書くとやりがちな気がします。

DispatchQueueとの比較

長くなるので該当メソッドだけにします。
適当にボタンを作って割り当ててください。

    @objc func pushDispatchButton(_ sender: UIButton) {
        print("Push Dispatch")
        
        DispatchQueue.global().async {
            let text = self.dispatchWaitCountUp()
            
            DispatchQueue.main.async {
                self.label.text = text
                print("dispatchWaitCountUp後に実行")
            }
        }
        print("dispatchWaitCountUpと並列実行")
    }
    
    func dispatchWaitCountUp() -> String {
        sleep(5)
        self.count += 1
        return String(self.count)
    }

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

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

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

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

Actor

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

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

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

class ActorTestViewController:UIViewController {

//中略
    
    @objc func pushAsyncButton(_ sender: UIButton) {
        print("Push async")
        
        Task{
//            let text = await asyncWaitCountUp()
            let asyncClass = AsyncClass()
            let text = await asyncClass.asyncWaitCountUp(count: count)
            self.label.text = text
            print("asyncWaitCountUp後に実行")
        }
        print("asyncWaitCountUpと並列実行")
    }
}

class AsyncClass{
    
    func asyncWaitCountUp(count:Int) async -> String {
        sleep(5)
        return String(self.count + 1)
    }
}

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

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

actor AsyncClass{
    
    func asyncWaitCountUp(count:Int) async -> String {
        sleep(5)
        return String(self.count + 1)
    }
}

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

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

actor TestActor{
    var text = "Test"
}

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

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

コメント

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