Open1
Swift actor練習帳
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させたら先に終了してしまう
- awaitさせてるからその後絶対Counterのresponseがnilでないとあると思いこんでたが、guardされると2回めのタップはすぐに終了してくれるのでawaitは終了順番をロックしてくれるわけじゃない