123
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TL;DR:

Combine をやめて、Observation に移行しよう。


今年の WWDC では、アップルがさりげなく新しいデータ追跡フレームワークを公開しました:Observation です。

データ追跡と言えば、4 年前の WWDC にて、アップルが SwiftUI と Combine を同時に公開したのがまだ記憶に新しいです。新たな宣言型 UI パラダイムに、それに適したように見えるリアクティブ風1のデータ追跡仕組みが一瞬にして多くのアップルプラットフォーム開発者の心を奪い、一時期 RxSwift や ReactiveSwift から Combine への移行記事もたくさんの人気を集めました。

しかし情報2によると、SwiftUI と Combine はそれぞれ違うチームが開発しており、お互いの存在が WWDC まで知らなかったそうです。真偽はわかりませんが、ただ確かにそれならいくつかの疑問の説明がつきます。

先述通り、Combine はリアクティブ風の仕組みを提供するフレームワークです。これは非常に高機能なもので、データ追跡のみならず、オペレーターを通じた複雑なデータ加工や、非同期処理などの仕事もこなせます。なんなら Combine のドキュメント読んでみれば、本当に非常にたくさんの概念が入ってることがわかると思います。

Combine のドキュメント

ところがが改めて考えてみましょう:これらの機能は本当に SwiftUI に必要なものですか?

そうです。Combine にある多くの機能は実は SwiftUI には必要ないのです。それでもこのように作られているのは、もし SwiftUI と Combine の開発が別々だったという話が本当であれば、最初から Combine チームは具体的に何が必要なのかわからなかったので、とりあえずオーバーキルなものを作っておくしかない、という流れだった可能性が高いと思います。さらにいうと ObservableObject(Beta 時には BindableObject だった)が今 objectWillChange プロパティー持ってるのに、Beta の時は最初 objectDidChange だったのも、Combine の開発当初 SwiftUI の仕組みが分からなかったからこその過ちだったかもしれません。

そしてあれから 4 年が経ち、SwiftUI 自身もだんだん成熟してきたし、SwiftUI として本当に必要だったのはたったのデータ追跡機能だと把握したし、非同期処理には Structured Concurrency 機能も一昨年導入しましたから、アップルも実は Combine は目的に対して非常に肥大化していることも把握してるのではないかと思います。だから Combine の使命はもう終わったと言わんばかりに、Apple はついにデータ追跡だけに特化したフレームワーク Observation を公開しました。

名前から察せる通り、このフレームワークはリアクティブではなく、オブザーバーパターンに基づいた仕組みとなります。オブザーバーパターンと聞くと、昔からアップルプラットフォーム開発やってきた人は Cocoa の KVO と言う仕組みを思い出すかもしれません。このフレームワークがやってるのも、まさに似たようなことだと思います:Observation を使うと、監視対象のオブジェクトが変更されたら通知が届く。これ以上のことはやりません。非同期処理にも手を出さないし、ましてはリアクティブみたいなオペレーター繋いでデータ加工してどうのこうののことは絶対にあり得ない。このように専念することを絞った結果、Observation の機能は非常に単純で使うだけ3なら誰でもすぐに使い方を覚えられ、ドキュメントまでとてもスッキリしました。

Observation のドキュメント

では、実際に Observation を使えば、SwiftUI でのデータ監視は本当にさらに楽になるのか、これは下記のようなカウンター画面で、Combine の書き方と比べてみましょう。まずは我々が慣れてる Combine の書き方から:

Counter.swift
import Combine

final class Counter: ObservableObject {

    @Published var title = ""
    @Published var count = 0

}
CounterView.swift
import SwiftUI

struct CounterView: View {
    
    @StateObject private var counter = Counter()
    
    var body: some View {
        VStack {
            TextField(text: $counter.title) {
                Text("Counter Title")
            }
            Button {
                counter.count += 1
            } label: {
                Text("\(counter.count)")
            }
        }
    }
    
}

上記の場合、我々は Counter と言うデータモデルを定義し、中でタイトルとカウント数を管理しています。そしてそれを表示する CounterView を定義し、中で Counter インスタンスを @StateObject として保持し、TextField でカウンターのタイトルを設定したり、ボタン押すたびにカウント数を一つ上げるように設定します。とても慣れてる書き方ですね。

そしてこれを Observation で書くと、このようになります:

Counter.swift
import Observation

@Observable final class Counter {

    var title = ""
    var count = 0

}
CounterView.swift
import SwiftUI

struct CounterView: View {
    
    @State private var counter = Counter()
    // ... 以下略
}

差分だけ切り出すとこうなります:

Counter.swift
-import Combine
+import Observation


-final class Counter: ObservableObject {
+@Observable final class Counter {

-    @Published var title = ""
-    @Published var count = 0
+    var title = ""
+    var count = 0

}
CounterView.swift
import SwiftUI

struct CounterView: View {
    
-    @StateObject private var counter = Counter()
+    @State private var counter = Counter()
    // ... 以下略
}

ざっとみて、書く行数自体は確かに全然変わってないようですが、実はこの書き方は大きなメリットが少なくとも二つあります。まずは Counter と言うデータモデルに、監視するプロパティーの前にいちいち @Published を書かなくて済み、型の定義にだけ @Observable 書けばいいので、書く労力が多少減ります。そしてプログラミングの鉄則の一つとして、コードが少ないほどバグが少ないと言われています。なのでこれ一つの例では分かりにくいですが、大規模なプロジェクトならかなり大きな違いが現れると思われます。そしてもう一つのメリットは View 側のプロパティーアトリビュートが大きく減り、参照型のデータモデルでも値型のように扱えて、ほとんどの場合データモデルが値型か参照型かを意識する必要がなくなりました4。意識する必要があることも少ければ少ないほど、バグが減りやすいですので、とても有意義な改善だと筆者は思います。

そして実は書き方だけでなく、裏の動きもちょっと違いがあります。これまで Combine の時、監視するのはあくまで ObservableObject 全体であって、ObservableObjectobjectWillChange パブリッシャーを購読していたので、大規模なデータモデルを監視している場合、全く関係ないプロパティーの更新でも、View のレンダリングサイクルが発火される可能性があります。しかし Observation の場合、View が監視しているのは一つ一つのプロパティーに変わったので、関係ないプロパティーの変更が View のレンダリングを発火せず、つまりパフォーマンスの改善もあるのです。

ところが、例えば仮に自分がすでに ObservableObject に慣れており、Observation のメリットをそれほどのものと思っていなくても、Observation に移行すべきですか?筆者は迷いもなくイエス、と思います。

上にも書いた通り、Combine は SwiftUI のためのデータ追跡という目的に対して概念や仕組みが複雑すぎました。そしてこのことはおそらくアップルも把握していると思います。だからこそ今年は Observation をリリースしたし、そして実は今年の WWDC のセッションを調べてみると驚きの事実に気づきます:今年は Combine 関係のセッションが一つもありませんでした。それどころか、Combine から Observation への移行は Observation のセッション動画だけでなく、あれだかシンプルな Observation のドキュメントにまで移行手順が実は入っています。つまり Combine は、少なくとも SwiftUI の開発だけの目的なら、もう実質 Deprecated5 のようなものです。

現在唯一の問題は Observation の対応バージョンは iOS 17 以降限定ですが、もしいつかこの問題が解消されたら、間違いなく Observation の時代が来るでしょう。それに備えて、今すぐ Observation の概念や書き方を慣れておきましょう。

  1. あくまで「リアクティブ」と呼ぶのは、本格的なリアクティブプログラミングにある概念、例えば Hot/Cold が、Combine にない場合があるためです。

  2. 確か以前 Twitter で見かけた情報ですが、今探しても見つかりませんでした…もし見つかった人がいたらぜひ教えて欲しいです。

  3. ただし Observation の実現には Swift Macros の仕組みを利用しており、こちらは把握するにはそれなりに苦労します。

  4. 厳密にはまだちょっとあって、@Environment を使うときは KeyPath の代わりに型名を書かないといけないし、またバインディングの時も @Binding ではなく @Bindable を書く必要があるはずです。ただ筆者の環境で試してみた結果、@Observable オブジェクトを @Binding で受け取っても特に問題なく動き、ただし @Binding の場合は値型と同じく $ を先頭につけないといけないが、@Bindable の場合は $ をつける必要がないです。これ以外でもし何か @Binding を使うと不具合が起きるケースがあったらぜひ筆者に教えて欲しいです。

  5. もちろん、もしまだリアクティブ風の開発を行いたいなら、Combine は選択肢としてアリだとは思います。公式がまだ Combine を Deprecated にマークしていないのもこれが原因かもしれません。しかしアップルとして本当にリアクティブ風な開発を推したいかと言われると…どうでしょうね。

123
42
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
123
42

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?