このエラーに悩んだ。
Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
XCode を探すと紫色の警告が出ていた。(探さないと分からなかった)
どうやら、バックグラウンドの処理で、@Published がついた変数を変更するとこのエラーが出るようだ。
私が開発中のアプリは、アプリ起動直後にローディング画面を表示し、そのバックグラウンドで初期化処理を行なっている。 バックグラウンドから@Published 付き変数を無邪気に更新すると、上のエラーが出る。
バックグラウンドスレッドの作り方
そもそもだが、メインスレッドを使わず、バックグラウンドスレッドを作るには下のように書く。
Task を使う例
Task {
await initMethodA()
}
async let :() を使う例
async let initB: () = try initMethodB()
async let initC: () = try initMethodC()
_ = try await (initB, initC)
※initMethodB とinitMethodC が並列実行される
そして、バックグラウンドスレッドから以下のように変数を更新すると上のエラーになる。
func initMethodA() async throws {
someVM.publishedValue = "newValue"
}
バックグラウンドスレッドからMainActor で実行させる書き方
私の調査では2つのやり方が見つかった。
MainActor.run を使うやり方
func initMethodA() async throws {
await MainActor.run {
someVM.publishedValue = "newValue"
}
}
MainActor.run の内側はメインスレッドで実行される。 メインスレッドで変数を更新すればエラーは起きない。
気をつけなければならないのは、await を使うことができない点だ。 使いたい場合は、先にawait が必要な処理を行っておくとよい。
func initMethodA() async throws {
// 先に非同期処理を実行
let newValue = await someAsyncMethod()
await MainActor.run {
// メインスレッドは変数の代入のみ
someVM.publishedValue = newValue
}
}
Task { @MainActor in を使うやり方
Task { @MainActor in
someVM.publishedValue = await someAsyncMethod()
}
もう一つがこちら。await が使えるので使いやすい。コードも簡潔で見通しも良い。
しかし、Task がメインスレッドで実行されて、someAsyncMethod はメインか別スレッドのどちらかで実行されて、publishedValue への代入はメインスレッドで実行される。 MainActor.run に比べて、メインスレッドの出番が多い。
私が思う解決策
@Published にprivate(set) を追加することで、外部から変更不可にできる。代わりに、ViewModel にsetter を作成してsetter の中でMainActor.run を使うことで、呼び出し元がメインスレッドかどうかを考えずに済む。
class SomeViewModel: ObservableObject {
@Published private(set) var publishedValue = ""
func setValue(_ newValue: String) async {
await MainActor.run {
publishedValue = newValue
}
}
}
残念だがこれは完璧ではない。画面から更新する変数の場合、private(set) を付けられないことがある。例えばトグルの場合だ。
Toggle("有効にする",isOn: $someVM.publishedValue)
※publishedValue はView から更新する必要がある
このようなケースは仕方ないので、個別に対応するしかない。(と思う)
【広告】