Open1

Swift actor練習帳

yimajoyimajo

Swift Concurrencyのactorを試す

ViewControllerとCounter

登場する型

  • class ViewController
  • actor Counter

試すこと

ViewControllerからCounterの+, -を増減させてその都度、Web API呼び出しの副作用を実行してみる。

import UIKit

class ViewController: UIViewController {

    @IBOutlet private var countLabel: UILabel!
    @IBOutlet private var descriptionLabel: UILabel!
    private let counter = Counter()

    override func viewDidLoad() {
        super.viewDidLoad()
        assert(Thread.isMainThread, "もちろんこの処理はメインスレッド")
        descriptionLabel.text = "デフォルトで表示したいメッセージ"

        Task {            
            await counter.reset()
            countLabel.text = await "\(counter.count)" // Task awaitが必要。プロパティにアクセスするのも
        }
    }

    @IBAction func incrementButtonTouchUp(_ sender: Any) {
        Task {
            do {
                try await counter.increment()
                countLabel.text = "\(await counter.count)" // await "\(counter.count)" とも "\(await counter.count)" とも書ける。後者のほうが読みやすい
                assert(Thread.isMainThread, "この処理はメインスレッド")

                descriptionLabel.text = await counter.response!.text
            } catch {
                descriptionLabel.text = "エラー \(error.localizedDescription)"
            }
        }
    }
    @IBAction func decrementButtonTouchUp(_ sender: Any) {
        Task {
            do {
                try await counter.decrement()
                countLabel.text = "\(await counter.count)"
                assert(Thread.isMainThread, "この処理はメインスレッド")

                descriptionLabel.text = await counter.response!.text
            } catch {
                descriptionLabel.text = "エラー \(error.localizedDescription)"
            }
        }
    }
}

actor Counter

actor Counter {
    enum Status {
        case `default`
        case fetching
    }
    struct Response: Decodable {
        let text: String
    }

    var count = 0
    var response: Response?
    // 今は使ってない
    private var status = Status.default

    var url: URL {
        URL(string: "http://numbersapi.com/\(count)?json")!
    }

    // asyncキーワードは省略できる 
    func reset() {
        count = 0
    }

    func increment() async throws {
        count += 1
        try await fetchRequest()
    }

    func decrement() async throws {
        count -= 1
        try await fetchRequest()
    }

    func fetchRequest() async throws {
        guard status == .default else {
            // ここの分岐に入らず、すでに処理中な通信はawait中になっているが、
            // その最中にここのguard else条件に当てはまると即処理が終わってしまうので、
            // self.resultがnilな場合がある。
            print("Counter: 処理中なのでreturnした")
            return
        }
        print("Counter: 処理開始")
        defer {
            status = .default
        }
        status = .fetching
        let request = URLRequest(url: url)
        let (data, response) = try await URLSession.shared.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse,
            httpResponse.statusCode == 200 else {
            fatalError("エラーを用意するのめんどくさいからあとで。")
        }

        let decoded = try JSONDecoder().decode(Response.self, from: data)
        self.response = decoded
    }
}

わかること

  • 当たり前なこと
    • ViewControllerからactorのCounterプロパティにアクセスするたびにawait
    • actorのメソッドのasyncキーワードは省略できる
  • 気づかされたこと
    • awaitさせてるからその後絶対Counterのresponseがnilでないとあると思いこんでたが、guardされると2回めのタップはすぐに終了してくれるのでawaitは終了順番をロックしてくれるわけじゃない
      • 1度目の通信中、2度目のイベントが発生してguardさせたら先に終了してしまう