こんにちは。新人プログラマーの岩本です。
今回はObservableObjectがViewを再描画するまでの裏側の処理を解説したいと思います。
この記事を読むことでいつも書いてるコードの解像度が上がり、ObservableObjectをより使いこなせるようになります。
ぜひ最後までご覧ください。
実装環境
- 言語…Swift 5.8.1
- OS… macOS Sonoma 14.1.1
- アプリ… Xcode 14.3.1
ObservableObjectとは?
ObservableObjectとはデータの変更を監視してビューを更新するために使用されるプロトコルです。
SwiftUIを触ったことがある人なら、頻繁に目にしていると思います。
Viewの監視対象としたいプロパティには@Publishedを付加します。
ここは理解しているという前提なので、簡単な使用例を載せて進みます。
final class Counter: ObservableObject {
@Published var count = 0 // Publishedをつけることで変更を検知できる
}
struct CounterView: View {
@ObservedObject var counter = Counter()
var body: some View {
VStack {
Button("+") {
counter.count += 1
}
Text(counter.count.description)
}
}
}
どのようにしてViewが再描画されるのか?
ObservableObjectの役割は値の変更をViewに伝えることです。Viewはそれを受け取り、画面を再描画します。
ではそれを実現するために裏側ではどのような処理が行われているのでしょうか?
ここでObservableObjectの定義を見てみましょう。
public protocol ObservableObject : AnyObject {
/// The type of publisher that emits before the object has changed.
associatedtype ObjectWillChangePublisher : Publisher = ObservableObjectPublisher where Self.ObjectWillChangePublisher.Failure == Never
/// A publisher that emits before the object has changed.
var objectWillChange: Self.ObjectWillChangePublisher { get }
}
ObservableObjectはobjectWillChangeというPublisherを持っています。これはプロパティの値がが変更されたタイミングで発火するPublisherです。
ViewはこのobjectWillChangeをsubscribeすることで値の変更を検知し、再描画を行います。
具体的な実装の詳細は分かりませんが、例えばこんな感じです。
object.objectWillChange // objectは@ObservedObject指定したもの
.sink {
再描画()
}
.store(&cancellable)
@Publishedの役割
ObservableObjectでViewの監視対象としたいプロパティには@Publishedを付加します。これにはどんな意味があるのでしょうか?
実は@Publishedを付加すると、値の変更の際にobjectWillChangeを発火するコードが自動で補完されます。
なので逆に言えば@Publishedを用いずに、同じ動作を実装することができます。
final class SampleModel: ObservableObject {
var value: Int = 0 {
willSet {
objectWillChange.send()
}
}
// 👆は@Published var value: Int = 0 と同義
}
@Publishedを使わない実装例
@Publishedを使わずに手動でobjectWillChangeを発火するようにすると、特定のタイミングでViewを再描画させることができます。
また設計によっては@Publishedを使わない方がいい時もあります。
以下は手動でobjectWillChangeを発火している例です。ネットワークリクエストの進捗が50%を超えた時にViewを更新しています。
final class NetworkRequestManager: ObservableObject {
private var progressPercentage: Double = 0 {
willSet {
// 進捗が50%を超えたときにUIを更新
if progressPercentage > 0.5 {
objectWillChange.send() // 手動でobjectWillChangeを発火
}
}
}
func simulateNetworkRequest() {
// 仮想のネットワークリクエストを進める処理が成功したと仮定
progressPercentage += 0.1
}
}
ObservableObjectからの脱却
ここまでみてもらった通りObservableObjectにはCombineが使われています。Combineはデータ追跡だけでなく、データの加工や非同期処理などさまざまな概念を含んでいます。
しかしWWDC2023で、よりデータ追跡に特化したフレームワークのObservationが公開されました。Observationを使うことで、今までのコードをより簡潔に書き換えられます。
今までのCombine式(ObservableObject)ではこんな書き方でした。
final class Counter: ObservableObject {
@Published var count = 0
}
struct CounterView: View {
@ObservedObject var counter = Counter()
var body: some View {
VStack {
Button("+") {
counter.count += 1
}
Text(counter.count.description)
}
}
}
同じことをObservationでやると、こう書き換えることができます。
import Observation
@Observable
final class Counter {
var count = 0 // @Publishedを指定する必要なし
}
struct CounterView: View {
@State var counter = Counter() // @Stateで指定できる
var body: some View {
VStack {
Button("+") {
counter.count += 1
}
Text(counter.count.description)
}
}
}
Observationを使うことでコードの記述量が減るだけでなく、パフォーマンスも向上します。
現在ObservationはiOS17以上でしか使えませんが、対応バージョンが広がれば書き換えも進んでいくことが考えられます。
まとめ
今回はObservableObjectの裏側の処理について解説しました。
これを知っておくとコードの解像度が上がり、より使いこなせるようになります。
またObservationについても少し解説しました。今のうちにこの概念に慣れておくと、Combineから移行する際にやりやすくなると思います。
ぜひご自身の手で色々と試してみてください。
ここまでのご閲覧ありがとうございました!
コメント