@PublishedをSwiftUI以外で使ってはいけない
SwiftUIで使われる@Published
をCurrentValueSubject
の代わりに使えるというような記事を見かけたので、それはやめておいた方が良いということを書いておきます。
イベントのタイミングの違い
これが唯一の理由ではあるのですが、@Published
とCurrentValueSubject
では、Property Wrapperとクラスの違いだけではなく、Publisherとしてのイベントを発行する時のタイミングの違いがあります。@Published
は「値が変更される前(willSet)」でCurrentValueSubject
は「値が変更された後(didSet)」です。
コードを書いて動作を確認してみます。(Playgroundに貼り付ければそのまま実行できます)
import Combine
class Sample {
@Published var publishedValue : Int = 0
let currentValue: CurrentValueSubject<Int, Never> = .init(0)
func increment() {
publishedValue += 1
currentValue.value += 1
}
}
let sample = Sample()
var cancellables = Set<AnyCancellable>()
print("値の購読開始")
sample.$publishedValue.sink { received in
print("published received:\(received) property:\(sample.publishedValue)")
}.store(in: &cancellables)
sample.currentValue.sink { received in
print("currentValue received:\(received) property:\(sample.currentValue.value)")
}
print("値の変更")
sample.increment()
実行すると、以下のように出力されます。
値の購読開始
published received:0 property:0
currentValue received:0 property:0
値の変更
published received:1 property:0
currentValue received:1 property:1
@Published
だとsink
でイベントを受け取っている時点では、まだプロパティの値が変わっていないことがわかります。
なぜ@Published
が値がセットされる前にイベントを発行するのかといえば、SwiftUIで必要だからというのが大きな理由になると思います。
SwiftUIのUI更新の挙動
SwiftUIでは、監視対象となるObservableObject
に適合させたクラスのobjectWillChange
を呼び出すと、UIを更新する「予約」だけがされ、次のUI更新タイミングまで待ってから実際に必要な値を読み出しUIが更新されます。名前にもあるとおり「Will」なので、UI更新に必要な値が変更される「前」に呼び出すことが求められています。
ObservableObject
に適合させたクラスのプロパティに@Published
をつけると、そのプロパティの値の変更があったら自動でobjectWillChange
が呼び出されるようになっています。SwiftUI的に監視する値が変更される前にそのタイミングを受け取る必要があるから、@Published
も値の変更前にイベントを発行するわけです。
SwiftUI以外で@Publishedを使った場合
@Published
のプロパティは$
をつけることでPublisherを取得して購読することができます。
ただその時、値が変更される時のイベントを変更前のタイミングでしか受け取れないということは、イベントを受け取った側の処理にかなり気をつける必要があります。SwiftUIの場合はSwiftUIの中で処理が完結していて外部に影響は及びませんが、SwiftUI以外ではそうではありません。
sink
などのクロージャ内では自由に処理を書くことができますし、Publisherを別のプロパティでラップしたり他のSubjectでSubscribeした後は元が@Published
であったことが分からなくなります。その@Published
が隠蔽されたインターフェースを見て、イベントを受け取った時に「値が変更された」という意識でコードを書くと、アプリ全体としては変更前の値を保持している状態なので問題を引き起こすことがあります。
SwiftUIのような特殊な事情がなければ、ただ簡単に書けるからというだけで@Published
を選ぶべきではないと思います。
CurrentValueSubjectをProperty Wrapperで使う
とは言ってもCurrentValueSubject
をそのままで使うのはちょっとめんどくさいということであれば、Property Wrapperにして@Published
っぽく使えるようにする手もあります。
例えば、以下のようにProperty Wrapperを定義します。
import Combine
@propertyWrapper class CurrentValue<Value> {
init(wrappedValue: Value) {
self.projectedValue = .init(wrappedValue)
}
var wrappedValue: Value {
get { projectedValue.value }
set { projectedValue.value = newValue }
}
let projectedValue: CurrentValueSubject<Value, Never>
}
そうすると、プロパティに@CurrentValue
をつけることで内部的にCurrentValueSubject
で保持され、projectedValueでCurrentValueSubject
そのものにアクセスできるので、以下のように使うことができます。
class Sample {
@CurrentValue var value: Int = 0
}
let sample = Sample()
let cancellable = sample.$value.sink { value in
print("received:\(value) property:\(sample.value)")
}
sample.value = 1
実行すると、以下のように出力されます。
received:0 property:0
received:1 property:1
Discussion