伝えたいこと
-
ObservableObject
を適応した class でなくても@Published
は使用できる(前編の内容) -
『値を持つことができる』かつ『subscribeできる』という共通点が
CurrentValueSubject
と@Published
にはあり、大抵の場合で書き換えが可能である- 特に
@Published
の場合、private(set)
な Publisher を記述するときに簡単に書けるようになる(以下、サンプルコード参照)
- 特に
例えば、)
CurrentValueSubject
で書いていたこれや、
class Hoge {
private let mogemoge: CurrentValueSubject<Int, Never> = .init(0)
var mogemogePub: AnyPublisher<Int, Never> {
mogemoge.eraseToAnyPublisher()
}
}
CurrentValueSubject
で書いていたこれが、
class Hoge {
private let mogemoge: CurrentValueSubject<Int, Never> = .init(0)
let mogemogePub: AnyPublisher<Int, Never>
init() {
mogemogePub = mogemoge.eraseToAnyPublisher()
}
}
@Published
を使うと、こう書けるようになります。
class Hoge {
@Published private(set) var mogemoge: Int = 0
}
はじめに
前編でObservableObject
でなくても @Published
が使えることは分かったのですが、その使い道を検討します。
@Published
と CurrentValueSubject
は似ている件
まずは、前提として押さえてほしいこととして、@Published
と CurrentValueSubject
はかなり似ています。
CurrentValueSubject
の例
以下を満たすような適当な Hoge
クラスを用意します。
- subscribe 可能な
mogemoge
-
mogemoge
の値を更新する Setter であるsetMogemoge()
- 最新の
mogemoge
の値を使用する用途があると仮定したprintMogemoge()
class Hoge {
let mogemoge: CurrentValueSubject<Int, Never> = .init(0)
func setMogemoge(_ mogemoge: Int) {
self.mogemoge.value = mogemoge
}
func printMogemoge() {
print("mogemoge: \(mogemoge.value)")
}
}
この場合、Setter からも CurrentValueSubject
の .value
の両方で、mogemoge
の値の更新が可能になっています。
let hoge = Hoge()
// subscribe を実施
hoge.mogemoge
.sink { print("receive: \($0)") }
.store(in: &cancellables)
// Setter から値の更新が可能
hoge.setMogemoge(1)
// CurrentValueSubject のため mogemoge.value = xxx でも値の更新が可能
hoge.mogemoge.value = 2
// 現在の Hoge().mogemoge の値を出力する
hoge.printMogemoge()
// (出力)
// receive: 0
// receive: 1
// receive: 2
// mogemoge: 2
@Published
の場合
ここで先ほど CurrentValueSubject
で記述していた mogemoge
を @Published var
で書き直すと以下のようになります。
class Hoge {
// `@Published var` に変更🌟
@Published var mogemoge: Int = 0
func setMogemoge(_ mogemoge: Int) {
self.mogemoge = mogemoge
}
func printMogemoge() {
print("mogemoge: \(mogemoge)")
}
}
let hoge = Hoge()
// `$` で Publisher としてアクセス可能に🌟
hoge.$mogemoge
.sink { print("receive: \($0)") }
.store(in: &cancellables)
hoge.setMogemoge(1)
// @Published var mogemoge は外から代入可能🌟
hoge.mogemoge = 2
hoge.printMogemoge()
// (出力)
// receive: 0
// receive: 1
// receive: 2
// mogemoge: 2
このように『値を持つことができる』かつ『subscribeできる』という共通点が CurrentValueSubject
と @Published
にはあり、書き換えが可能です。
subscribe 可能な値を外部から編集されない方法を検討する
上記で説明したように @Published
と CurrentValueSubject
は似ているのですが、private
のアクセス修飾子をつけてみると挙動の差がでてきます。
例えば先ほどの Hoge
クラスについて、『mogemoge
は外部からは値を直接編集できないようにする』という条件を加えます。
- subscribe 可能な
mogemoge
-
mogemoge
の値を更新する Setter であるsetMogemoge()
- 最新の
mogemoge
の値を使用する用途があると仮定したprintMogemoge()
-
mogemoge
は外部から値を直接編集できないようにする ← New
つまり、mogemoge
は setMogemoge()
のインターフェースでのみ値の変更をさせたいとします。
そのときに、@Published
と CurrentValueSubject
で実装しようとすると差が出てきます。
方法1: ❌ CurrentValueSubject
の変数に private
をつけた場合
まず、CurrentValueSubject
の変数に private
をつけてみます。
class Hoge {
// private のアクセス修飾子をつける
private let mogemoge: CurrentValueSubject<Int, Never> = .init(0)
func setMogemoge(_ mogemoge: Int) {
self.mogemoge.value = mogemoge
}
func printMogemoge() {
print("mogemoge: \(mogemoge.value)")
}
}
確かに以下のように hoge.mogemoge.value = xxx
での変更はできなくなります。
しかし、残念ながら subscribe もできなくなりました😭
let hoge = Hoge()
// subscribe できなくなった😭(コンパイルエラー)
hoge.mogemoge // 'mogemoge' is inaccessible due to 'private' protection level
.sink { print("receive: \($0)") }
.store(in: &cancellables)
hoge.setMogemoge(1)
// hoge.mogemoge.value = xxx で変更できなくなった ← 狙い通り😎
hoge.mogemoge.value = 2 // 'mogemoge' is inaccessible due to 'private' protection level
hoge.printMogemoge()
当たり前の挙動ですね。
次の手として、private(set)
を検討します。
方法2: ❌ CurrentValueSubject
の変数に private(set)
をつけた場合
mogemoge
に private(set)
のアクセス修飾子をつけます。
また、 private(set) let
とはできないため、let
を var
に変更します。
これでなら、subscribe を許可したまま、値の直接編集を制限できそうです。
class Hoge {
// CurrentValueSubject を private(set) な変数とする
private(set) var mogemoge: CurrentValueSubject<Int, Never> = .init(0)
func setMogemoge(_ mogemoge: Int) {
self.mogemoge.value = mogemoge
}
func printMogemoge() {
print("mogemoge: \(mogemoge.value)")
}
}
残念ながら、そうはなりませんでした😭
以下をご覧ください。
let hoge = Hoge()
// subscribe できる🌟
hoge.mogemoge
.sink { print("receive: \($0)") }
.store(in: &cancellables)
hoge.setMogemoge(1)
// private(set) でも `hoge.mogemoge.value = xxx` してもコンパイルにならない😭
hoge.mogemoge.value = 2
hoge.printMogemoge()
// (出力)
// receive: 0
// receive: 1
// mogemoge: 1
// mogemoge: 2
hoge.mogemoge.value = xxx
で更新できてしまいました😭
↑こうなる解説(private(set) var
の挙動について)
実はこれは当たり前のことで、以下のサンプルコードのように、クラスの更新ができないだけで、そのクラスのもつ変数は更新することができます。
class Moge {
var piyopiyo: Int = 0
}
class Hoge {
private(set) var moge: Moge = .init()
}
let hoge = Hoge()
// これはコンパイルエラー
hoge.moge = Moge() // Cannot assign to property: 'moge' setter is inaccessible
// コンパイルエラーとはならない
hoge.moge.piyopiyo = 1
CurrentValueSubject
の公式ドキュメントをみるとわかるのですが、CurrentValueSubject
実はクラスです。
final class CurrentValueSubject<Output, Failure> where Failure : Error
なので、上記のサンプルコードと同じように private(set)
でも値を更新できてしまうのです。
方法3: ⭕️ 別口の AnyPublisher
を公開する(その1)
先ほどの CurrentValueSubject
の mogemoge
に private
のアクセス修飾子をつけることに加えて、mogemoge
とは別口で AnyPublisher
でラッピングした mogemogePub
として公開させます。
方法は以下の 2 種類あるのですが、今回は『init()
で設定する』を紹介します。
- その1:
init()
で設定する ← こっち - その2:Computed property で設定する
class Hoge {
private let mogemoge: CurrentValueSubject<Int, Never> = .init(0)
// subscribe 専用の mogemogePub を用意
let mogemogePub: AnyPublisher<Int, Never>
init() {
// init() で設定
mogemogePub = mogemoge.eraseToAnyPublisher()
}
func setMogemoge(_ mogemoge: Int) {
self.mogemoge.value = mogemoge
}
func printMogemoge() {
print("mogemoge: \(mogemoge.value)")
}
}
mogemoge
でのアクセスを制限したまま、mogemogePub
を subscribe させます。
let hoge = Hoge()
// mogemogePub なら subscribe できる🌟
hoge.mogemogePub
.sink { print("receive: \($0)") }
.store(in: &cancellables)
hoge.setMogemoge(1)
// 以下でアクセスできずコンパイルエラー ← 狙い通り😎
// hoge.mogemoge.value = 2 // 'mogemoge' is inaccessible due to 'private' protection level
hoge.printMogemoge()
// (出力)
// receive: 0
// receive: 1
// mogemoge: 1
setMogemoge()
のインターフェースでのみ値の変更に制限できて、CurrentValueSubject
の .value = xxx
でのアクセスを禁止することに成功しました。
方法4: ⭕️ 別口の AnyPublisher
を公開する(その2)
続いて『Computed property で設定する』の方も紹介します。
ちなみに私はこちらの方が好みです。
- その1:
init()
で設定する - その2:Computed property で設定する ← こっち
class Hoge {
private let mogemoge: CurrentValueSubject<Int, Never> = .init(0)
// init() で設定せずに Computed property にすることも可能(ただし var となる)
var mogemogePub: AnyPublisher<Int, Never> {
mogemoge.eraseToAnyPublisher()
}
func setMogemoge(_ mogemoge: Int) {
self.mogemoge.value = mogemoge
}
func printMogemoge() {
print("mogemoge: \(mogemoge.value)")
}
}
先ほどと同様に、mogemoge
でのアクセスを制限したまま、mogemogePub
を subscribe させます。
let hoge = Hoge()
// mogemogePub なら subscribe できる🌟
hoge.mogemogePub
.sink { print("receive: \($0)") }
.store(in: &cancellables)
hoge.setMogemoge(1)
// 以下でアクセスできずコンパイルエラー ← 狙い通り😎
// hoge.mogemoge.value = 2 // 'mogemoge' is inaccessible due to 'private' protection level
hoge.printMogemoge()
// (出力)
// receive: 0
// receive: 1
// mogemoge: 1
方法4 と同様にこれでもいけますね。
方法5: ❌ @Published
の変数に private
をつけた場合
方法1 を CurrentValueSubject
ではなく @Published
で書いた場合です。
class Hoge {
// @Published private をつける🌟
@Published private var mogemoge: Int = 0
func setMogemoge(_ mogemoge: Int) {
self.mogemoge = mogemoge
}
func printMogemoge() {
print("mogemoge: \(mogemoge)")
}
}
もちろん、方法1 と同様に subscribe できなくなります😭
let hoge = Hoge()
// subscribe できなくなった😭(コンパイルエラー)
hoge.$mogemoge // '$mogemoge' is inaccessible due to 'private' protection level
.sink { print("receive: \($0)") }
.store(in: &cancellables)
hoge.setMogemoge(1)
// mogemoge = xxx で変更できなくなった ← 狙い通り😎
hoge.mogemoge = 2 // 'mogemoge' is inaccessible due to 'private' protection level
hoge.printMogemoge()
方法6: ⭕️ @Published
の変数に private(set)
をつけた場合
方法2 を CurrentValueSubject
ではなく @Published
で書いた場合です。
class Hoge {
// @Published private(set) をつける🌟
@Published private(set) var mogemoge: Int = 0
func setMogemoge(_ mogemoge: Int) {
self.mogemoge = mogemoge
}
func printMogemoge() {
print("mogemoge: \(mogemoge)")
}
}
どうせ、方法2 の CurrentValueSubject
みたくうまくいかないのだろうと思ったのですが、これがうまくいきます。
let hoge = Hoge()
// @Published private(set) なら subscribe できる🌟
hoge.$mogemoge
.sink { print("receive: \($0)") }
.store(in: &cancellables)
hoge.setMogemoge(1)
// 以下は狙い通りコンパイルエラー😎
// hoge.mogemoge = 2 // Cannot assign to property: 'mogemoge' setter is inaccessible
hoge.printMogemoge()
// (出力)
// receive: 0
// receive: 1
// mogemoge: 1
CurrentValueSubject
の場合は mogemoge.value
の値を更新していたので、private(set) var
では制限できませんでしたが、@Published
の場合は、mogemoge
の値を直接編集しに行っているので、ちゃんと制限できるようです。
これであれば、方法3 や方法4 のように別口の AnyPublisher
を公開する必要もないので、シンプルに実装できますね。
この辺のアクセス制御は CurrentValueSubject
にはできない @Published
の使い道となります。
他にも、CurrentValueSubject
の記述がシンプルになる例があるかもしれませんね。
PassthroughSubject
も @Published
で書き直せるのか?
CurrentValueSubject
の方法4 で行っていたことを PassthroughSubject
で書き直してみます。
class Hoge {
private let mogemoge = PassthroughSubject<Int, Never>()
var mogemogePub: AnyPublisher<Int, Never> {
mogemoge.eraseToAnyPublisher()
}
// `setMogemoge()` ではなく `sendMogemoge()` が適切だと思いますが。
func setMogemoge(_ mogemoge: Int) {
self.mogemoge.send(mogemoge)
}
// PassthroughSubject では値を保持できないのでこれに相当する処理が記述できない
// func printMogemoge() {
// print("mogemoge: \(mogemoge.value)")
// }
}
let hoge = Hoge()
hoge.mogemogePub
.sink { print("receive: \($0)") }
.store(in: &cancellables)
hoge.setMogemoge(1)
// (出力)
// receive: 1
これを方法6 の @Published
の変数に private(set)
で書いたバージョンで書き直せるかどうかについて検討します。
個人的には PassthroughSubject
は現在値を保有していないため、CurrentValueSubject
や @Published
と全く性質の異なるものであり、単に記述量が減るからといって、@Published
に書き直したりはしないほうが適切だと思います。
結論
-
ObservableObject
を適応した class でなくても@Published
は使用できる(前編の内容) -
『値を持つことができる』かつ『subscribeできる』という共通点が
CurrentValueSubject
と@Published
にはあり、大抵の場合で書き換えが可能である- 特に
@Published
の場合、private(set)
な Publisher を記述するときに簡単に書けるようになる(以下、サンプルコード参照)
- 特に
例えば、)
CurrentValueSubject
で書いていたこれや、
class Hoge {
private let mogemoge: CurrentValueSubject<Int, Never> = .init(0)
var mogemogePub: AnyPublisher<Int, Never> {
mogemoge.eraseToAnyPublisher()
}
}
CurrentValueSubject
で書いていたこれが、
class Hoge {
private let mogemoge: CurrentValueSubject<Int, Never> = .init(0)
let mogemogePub: AnyPublisher<Int, Never>
init() {
mogemogePub = mogemoge.eraseToAnyPublisher()
}
}
@Published
を使うと、こう書けるようになります。
class Hoge {
@Published private(set) var mogemoge: Int = 0
}
以上になります。