Sponsor Link
環境&対象
- macOS Monterey 12.3 beta4
- Xcode 13.2.1
- iOS 15.2
これまでの記事は、以下です。
[SwiftUI][Swift] MVVM と Swift Concurrency を組み合わせる(2: View/ViewModel から actor にアクセスする)
[SwiftUI][Swift] MVVM と Swift Concurrency を組み合わせる(1: Model を actor に)
Actor の変数を監視
前回では actor の値を取得して、ViewModel の 変数を更新するために メソッドを作成して行っていました。
大抵のケースでは、変数が更新されるタイミングは明確なので、更新のたびにメソッドを呼び出しても良いのですが、せっかくなので、Combine をつかって更新処理をしてみます。
actor の変数を @Published に
Combine で仕組みとして用意されている @Published を使用します。
今回のケースでは、Model(actor Countdown) の count プロパティを @Published 指定して Publisher として使用します。
前回 作成した actor を以下のように修正します。(count に @Published 指定を付与しています)
//
// Countdown.swift
//
// Created by : Tomoaki Yagishita on 2022/03/01
// © 2022 SmallDeskSoftware
//
import Foundation
actor Countdown {
@Published var count: Int = 0
init(_ count: Int) {
precondition(count >= 0)
self.count = count
}
func decrement() {
if count == 0 { return }
count -= 1
}
func increment() {
count += 1
}
}
前回からの相違点は、以下の通りです。
・count に @Published を付与した。
@Published は、付与された変数の Publisher を用意する Property Wrapper なので 実は View に直接関係しないところでも便利に使えます。
以下の記事で説明してます。
[SwiftUI][Combine] @Published は、Publisher を提供する Property Wrapper
Publisher を Subscribe する
@Published を付与したことで、Publisher を使うことができるようになりましたので、ViewModel 側で Subscribe して変更された時に更新するようにします。
actor であることを意識せずに書くと以下のようになります。(Countdown が class であれば普通に使えます)
ViewModel の init 周辺だけ抜粋
class ViewModel: ObservableObject {
let countdown = Countdown(60)
@Published var count: Int
var cancellables: Set = Set()
init() {
count = 0
countdown.$count
.sink{ newValue in
print("newValue \(newValue)")
}
.store(in: &cancellables)
}
// snip
上記のコードでは以下のコンパイルエラーになります。
actor-isolated property '$count' can not be referenced from a non-isolated context
actor が保有するプロパティは、Publisher であっても、自由にアクセスできないルールは同じです。
ですので、async アクセスが必要となります。
呼ぼうとしている init は、sync なメソッドです。そのままでは async なアクセスはできないので、Task を作成してアクセスする必要があります。
ということで、以下のようになります。
ViewModel の init だけ抜粋
init() {
count = 0
Task {
await countdown.$count
.sink{ newValue in
print("newValue \(newValue)")
}
.store(in: &cancellables)
}
}
動作させてみると確認できますが、値が更新されるたびに、”newValue ~数字~” と Xcode のコンソールに表示されます。
以前は、decrement の処理後に、ViewModel 内から updateAsync を呼び出して ViewModel の持つ count を更新していましたが、この sink 内で更新するようにすれば、decrement 内で updateAsync を明示的に呼び出す必要はなくなります。
改良した init
init() {
count = 0
Task {
await countdown.$count
.sink{ newValue in
Task {
await self.updateAsync()
}
}
.store(in: &cancellables)
}
}
.sink の中は sync なので、async なメソッドを呼び出すには、Task を生成する必要があります。
まとめ
actor からも @Published で Publisher を公開できることを確認しました。
- 普通に、@Published と付与すれば、Publisher を作成できる
- ただし .sink するためには、async アクセスが必要
- .sink で動作させるコードの context (thread) に注意が必要なケースもある
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
Sponsor Link