[Swift] [Combine] ObservableObject ではない @Published の使い所


# 伝えたいこと

- `ObservableObject` を適応した class でなくても `@Published` は使用できる（[前編](https://zenn.dev/ikuraikura/articles/2022-02-23-pub)の内容）
- **『値を持つことができる』かつ『subscribeできる』という共通点が `CurrentValueSubject` と `@Published` にはあり、大抵の場合で書き換えが可能である**
  - 特に `@Published` の場合、`private(set)` な Publisher を記述するときに簡単に書けるようになる（以下、サンプルコード参照）

例えば、）

`CurrentValueSubject` で書いていたこれや、

```swift
class Hoge {
    private let mogemoge: CurrentValueSubject<Int, Never> = .init(0)
    
    var mogemogePub: AnyPublisher<Int, Never> {
        mogemoge.eraseToAnyPublisher()
    }
}
```

`CurrentValueSubject` で書いていたこれが、

```swift
class Hoge {
    private let mogemoge: CurrentValueSubject<Int, Never> = .init(0)
    
    let mogemogePub: AnyPublisher<Int, Never>
    
    init() {
        mogemogePub = mogemoge.eraseToAnyPublisher()
    }
}
```

`@Published` を使うと、こう書けるようになります。

```swift
class Hoge {
    @Published private(set) var mogemoge: Int = 0
}
```

# はじめに

[前編](https://zenn.dev/ikuraikura/articles/2022-02-23-pub)で`ObservableObject` でなくても `@Published` が使えることは分かったのですが、その使い道を検討します。

# `@Published` と `CurrentValueSubject` は似ている件

まずは、前提として押さえてほしいこととして、`@Published` と `CurrentValueSubject` はかなり似ています。

## `CurrentValueSubject` の例

以下を満たすような適当な `Hoge` クラスを用意します。

- subscribe 可能な `mogemoge`
- `mogemoge` の値を更新する Setter である `setMogemoge()`
- 最新の `mogemoge` の値を使用する用途があると仮定した `printMogemoge()`

```swift
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` の値の更新が可能になっています。

```swift
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` で書き直すと以下のようになります。

```swift
class Hoge {
    // `@Published var` に変更🌟
    @Published var mogemoge: Int = 0
    
    func setMogemoge(_ mogemoge: Int) {
        self.mogemoge = mogemoge
    }
    
    func printMogemoge() {
        print("mogemoge: \(mogemoge)")
    }
}
```

```swift
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` をつけてみます。

```swift
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 もできなくなりました😭

```swift
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 を許可したまま、値の直接編集を制限できそうです。

```swift
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)")
    }
}
```

残念ながら、そうはなりませんでした😭

以下をご覧ください。

```swift
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` の挙動について）

実はこれは当たり前のことで、以下のサンプルコードのように、クラスの更新ができないだけで、そのクラスのもつ変数は更新することができます。

```swift
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` の[公式ドキュメント](https://developer.apple.com/documentation/combine/currentvaluesubject)をみるとわかるのですが、`CurrentValueSubject` 実はクラスです。

```swift
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 で設定する

```swift
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 させます。

```swift
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 で設定する ← こっち**

```swift
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 させます。

```swift
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` で書いた場合です。

```swift
class Hoge {
    // @Published private をつける🌟
    @Published private var mogemoge: Int = 0
    
    func setMogemoge(_ mogemoge: Int) {
        self.mogemoge = mogemoge
    }
    
    func printMogemoge() {
        print("mogemoge: \(mogemoge)")
    }
}
```

もちろん、方法1 と同様に subscribe できなくなります😭

```swift
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` で書いた場合です。

```swift
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` みたくうまくいかないのだろうと思ったのですが、これがうまくいきます。

```swift
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` で書き直してみます。

```swift
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` は使用できる（[前編](https://zenn.dev/ikuraikura/articles/2022-02-23-pub)の内容）
- **『値を持つことができる』かつ『subscribeできる』という共通点が `CurrentValueSubject` と `@Published` にはあり、大抵の場合で書き換えが可能である**
  - 特に `@Published` の場合、`private(set)` な Publisher を記述するときに簡単に書けるようになる（以下、サンプルコード参照）

例えば、）

`CurrentValueSubject` で書いていたこれや、

```swift
class Hoge {
    private let mogemoge: CurrentValueSubject<Int, Never> = .init(0)
    
    var mogemogePub: AnyPublisher<Int, Never> {
        mogemoge.eraseToAnyPublisher()
    }
}
```

`CurrentValueSubject` で書いていたこれが、

```swift
class Hoge {
    private let mogemoge: CurrentValueSubject<Int, Never> = .init(0)
    
    let mogemogePub: AnyPublisher<Int, Never>
    
    init() {
        mogemogePub = mogemoge.eraseToAnyPublisher()
    }
}
```

`@Published` を使うと、こう書けるようになります。

```swift
class Hoge {
    @Published private(set) var mogemoge: Int = 0
}
```

以上になります。