この記事では、Swift の並行処理の新しいアプローチとして、ObservableObject に対して @MainActor を付けず、代わりに actor を利用する方法を、先輩: と 後輩: の対話形式で解説します。先輩の教えを聞きながら、並行処理の基本や設計の考え方、そしてなぜこれで安全なのかを一緒に学んでいきましょう。
はじめに
SwiftUI を使ったアプリ開発では、画面の更新がとても大事です。
ObservableObject は、データが変わると自動で View に通知して画面を更新してくれる仕組みです。通常は @MainActor を付けることで、すべての処理がメインスレッドで実行されるようにして UI の安全性を確保しています。しかし、これだと重い処理がメインスレッドを占有してしまい、UI が固まるリスクがあります。そこで、actor を使って並行処理を安全に行う新しい方法が登場しました。
先輩: と 後輩: の対話で学ぶ
後輩:
「先輩、ObservableObject って何に使うんですか?」
先輩:
「ObservableObject は、SwiftUI の仕組みでデータが変わったときに View に自動で更新を通知するためのものなんだ。例えば、ユーザーの入力やネットワークからのデータ更新があったときに使うよ。」
後輩:
「なるほど。で、なんで普通は @MainActor を付けるんですか?」
先輩:
「UI の更新は必ずメインスレッドで行わないといけないからね。@MainActor を付けると、ObservableObject 内のすべての処理がメインスレッドで実行されるようになるんだ。これで UI の不整合やデータ競合を防げるんだよ。」
後輩:
「でも、最近は actor を使う方法もあるって聞きました。actor って何ですか?」
先輩:
「actor は Swift の新しい並行処理の仕組みで、内部のデータへのアクセスを順番に処理する仕組みなんだ。これにより、複数のタスクが同時に同じデータを書き換えようとしても、アクターが value の同時書き換えを防いでくれるので、実際の競合防止は actor の担当になるんだ。ちなみに、**@Published は『UI への通知手段』**に過ぎないから、直接データ競合を防ぐ役割はないよ。」
後輩:
「つまり、メインアクター(@MainActor)を付けなくても、actor を使えばバックグラウンド処理で値をいじっても安全なんですね?」
先輩:
「その通り!actor を使うことで、重い処理はバックグラウンドで安全に実行でき、UI の更新だけをメインスレッドで行えば十分安全なんだ。これが、なぜ『これで安全なのか』という理由なんだよ。」
なぜこれで安全なの?
この設計が安全である理由は以下の通りです:
-
アクターが value の同時書き換えを防いでくれる:
actor は内部の状態へのアクセスをシリアル化して処理するため、複数のタスクが同時に同じデータを変更しようとしても、必ず順番に処理されます。これにより、データ競合(レースコンディション)が防止されます。 -
@Published は UI への通知手段:
@Published は ObservableObject のプロパティが変化した際に View へ通知する役割を持っていますが、実際のデータの安全な更新は actor に任せることで実現されます。つまり、@Published 自体はデータ競合の防止には関与していません。 -
バックグラウンド処理でも安全:
actor を使えば、メインスレッド(@MainActor)を付けなくても、バックグラウンドでの重い処理中に値が変更されても、actor 内で順序良く処理されるため、安全にデータの更新が行えます。
コード例で学ぶ
① 従来の @MainActor を使った方法
ここでは、すべての処理がメインスレッドで実行されるシンプルな方法を説明しています。
ただし、重い処理もメインスレッドで行われるため、処理が重いと UI が固まってしまう可能性があります。
import SwiftUI
@MainActor
class MyViewModel: ObservableObject {
@Published var data: String = "初期値"
// データを非同期で読み込む関数
func loadData() async {
// ネットワーク通信や重い処理もメインスレッドで実行される
let newData = await fetchData()
data = newData
}
// 2秒待機して新しいデータを返すシミュレーション
func fetchData() async -> String {
await Task.sleep(2 * 1_000_000_000) // 2秒待機
return "新しいデータ"
}
}
先輩:
「この方法はシンプルで直感的だけど、もし処理が重くなると UI の応答性に影響が出るリスクがあるんだ。」
② actor を使った方法
次は、actor を使って重い処理をバックグラウンドで安全に実行し、UI 更新時だけメインスレッドに切り替える方法です。
import SwiftUI
// 重い処理を安全に行うための Actor を定義
actor DataProcessor {
func processData() async -> String {
await Task.sleep(2 * 1_000_000_000) // 2秒待機
return "重い処理で得たデータ"
}
}
class MyViewModel: ObservableObject {
@Published var data: String = "初期値"
let processor = DataProcessor() // Actor のインスタンスを生成
func loadData() async {
// actor 内で重い処理を実行
let newData = await processor.processData()
// UI の更新はメインスレッドで行う必要があるので、MainActor.run を使って切り替える
await MainActor.run {
self.data = newData
}
}
}
先輩:
「この方法では、actor が内部でデータの同時書き換えを防いでくれるので、@Published による通知はそのまま使いつつも、バックグラウンドでの重い処理が安全に実行できるんだ。UI の更新は必要に応じてメインスレッドで行うようにすれば、全体の動作は非常に安定するよ。」
どちらの方法を選ぶべきか?
後輩:
「先輩、どっちの方法がいいんですか?」
先輩:
「それはアプリの内容次第だね。
- シンプルなアプリや処理が軽い場合は、@MainActor を使うとコードもシンプルで、初心者にも扱いやすい。
- でも、重い処理がある場合や、多くのタスクが同時に走る状況では、actor を使ってバックグラウンドで安全に処理を実行し、UI の更新だけをメインスレッドで行う方法が効果的だよ。」
まとめ
今回の対話を通じて、以下のポイントを学びました:
-
ObservableObject と @MainActor:
データが変わると自動で UI を更新してくれるが、重い処理があるとメインスレッドがブロックされるリスクがある。 -
actor を使った設計:
actor は内部で value の同時書き換えを防いでくれるため、実際の競合防止は actor の役割となる。@Published は UI への通知手段であり、データ競合の防止自体は actor に任せることで、バックグラウンド処理で値をいじっても安全に動作する。
どちらのアプローチにも一長一短があるため、実際のプロジェクトの要求に合わせて適切な方法を選ぶことが大切です。先輩の話を参考に、ぜひ自分に合った実装パターンを試してみてくださいね!
Happy Coding!