CombineのObservableObjectでinputとoutputを明確にしたい

## はじめに

SwiftUIでも少しでも複雑な画面になってくるとCombineのObservableObjectの入力と出力を分けたいときがあるわけです。ぱっと見で何が入力で出力なのか整理したいし、他の人にも整理してコードを書いてほしい。

RxSwiftのときは[RxSwift研究読本3 ViewModel設計パターン入門](https://booth.pm/ja/items/1223536)のようにRxCocoaがあるので工夫ができるわけだけど、SwiftUI+Combineではどうしていくと整理されるんだろうというわけで、この記事は簡単なサンプルを作って複数のやり方を示してみます。

## かんたんな画面例

2つのTextFieldがあってその2つの文字が同じ場合にButtonを押せるような画面を例とします。

<img width="384" alt="スクリーンショット 2020-04-19 20.46.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/16400/0fcd114f-e9fc-eb11-beec-54a067a88e80.png">

ボタンを押したときにTextFieldの値を見張るのではなく、TextFieldの文字をリアルタイムで見張りたいのです。

## どうやるか

雑に3つくらい考えられると思います

- 何も考えずコメントでinputやoutputとか書いてみる例
- input/outputを型にしてみる例
- PublishedをやめてSubjectを使う例

### 何も考えずコメントでinput/outputとか書いてみる例

まず、もう何も考えずにやれるようにやっていく例。こうやってしまうと、`@Published`が増えてinputかoutputか分かりづらい。

```swift
import Combine

// 何も考えずコメントでinput/outputとか書いてみる
class ObservableObject1: ObservableObject {
    // input
    @Published var password = ""
    @Published var repeatedPassword = ""

    // output
    @Published var validatedPassword = false

    private var cancellableSet: Set<AnyCancellable> = []

    init() {
        Publishers.CombineLatest($password, $repeatedPassword)
            .map { $0 == $1 }
            .eraseToAnyPublisher()
            .assign(to: \.validatedPassword, on: self)
            .store(in: &cancellableSet)
    }
}
```

SwiftUI側はこんな感じ

```swift
import SwiftUI

struct ContentView: View {
    @ObservedObject var object = ObservableObject1()

    var body: some View {
        VStack {
            TextField("パスワード", text: $object.password)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            TextField("パスワード繰り返して！", text: $object.repeatedPassword)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            Button(action: {
                print("押されたよ")
            }) {
                Text("パスワードが一緒だと押せるよ！")
            }
            .disabled(!object.validatedPassword)
        }
    }
}
```

- 長所
    - 最初は何も考えないで済む
- 短所
    - 数が増えると整理されてなさがきつい
    - inputは外から読まれる
    - input用に`@Published`することで2-way Bindingしてる
        - ObservableObject側で変更しないわけで、`@Published`の意味がない
            - `@Published`はViewを更新させる
                - 参考: [SwiftUIガイドブック](https://booth.pm/ja/items/1829015)

### input/outputを型にしてみる例

これをRxSwift使ってたときのようにちょっと工夫してみる。`Input`と`Output`型作ってみる。

```swift
import Combine

// InputとOutputをなるべく分けたいので型にした
class ObservableObject2: ObservableObject {
    class Input {
        @Published var password = ""
        @Published var repeatedPassword = ""
    }

    class Output {
        @Published var validatedPassword = false
    }

    // input
    var input: Input
    // output
    var output: Output

    private var cancellableSet: Set<AnyCancellable> = []

    init(input: Input = Input(), output: Output = Output()) {
        self.input = input
        self.output = output

        Publishers.CombineLatest(self.input.$password, self.input.$repeatedPassword)
            .map { $0 == $1 }
            .eraseToAnyPublisher()
            .assign(to: \.validatedPassword, on: self.output)
            .store(in: &cancellableSet)
    }
}
```

SwiftUI側は次のような感じ

```swift
import SwiftUI

struct ContentView2: View {
    @ObservedObject var object = ObservableObject2()

    var body: some View {
        VStack {
            TextField("パスワード", text: $object.input.password)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            TextField("パスワード繰り返して！", text: $object.input.repeatedPassword)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            Button(action: {
                print("押されたよ")
            }) {
                Text("パスワードが一緒だと押せるよ！")
            }
            .disabled(!object.output.validatedPassword)
        }
    }
}
```

- 長所
    - 型で整理された
- 短所
    - 単にまとめただけなのでInputの`@Published`の意味がさらによくわからない感じになる
    - inputは外から読まれる

### PublishedをやめてSubjectを使う例

とにかくinput側の`@Published`が嫌なのかもしれない。プロパティが更新されたらSubjectを呼び出すようにする。

```swift
import Combine

// inputからPublishedをなくせばいい
class ObservableObject3: ObservableObject {

    private let passwordSubject = PassthroughSubject<String, Never>()
    private let repeatedPasswordSubject = PassthroughSubject<String, Never>()

    // MARK: - input

    var password = "" {
        didSet {
            passwordSubject.send(password)
        }
    }

    var repeatedPassword = "" {
        didSet {
            repeatedPasswordSubject.send(repeatedPassword)
        }
    }

    // MARK: - output

    @Published var validatedPassword = false

    // MARK: -

    private var cancellableSet: Set<AnyCancellable> = []

    init() {
        Publishers.CombineLatest(passwordSubject, repeatedPasswordSubject)
            .map { $0 == $1 }
            .eraseToAnyPublisher()
            .assign(to: \.validatedPassword, on: self)
            .store(in: &cancellableSet)
    }
}
```

SwiftUI側はこんな感じ。最初の例と同じ。

```swift
import SwiftUI

struct ContentView3: View {
    @ObservedObject var object = ObservableObject3()

    var body: some View {
        VStack {
            TextField("パスワード", text: $object.password)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            TextField("パスワード繰り返して！", text: $object.repeatedPassword)
                .textFieldStyle(RoundedBorderTextFieldStyle())

            Button(action: {
                print("押されたよ")
            }) {
                Text("パスワードが一緒だと押せるよ！")
            }
            .disabled(!object.validatedPassword)
        }
    }
}
```

- 長所
    - `@Published`が付いているのは出力だけ
- 短所
    - 外部からinputは読まれる


## まとめ

- 一番最初の何も考えない例から、最後の`@Published`をやめてSubjectを使う例に移行するのはUI側を変えないので楽
    - 2-way Bindingしないなら出力にのみ`@Published`が付いているほうがコードを把握しやすい
    - しかし結局inputは外部から読まれる（読んで何が困るということはないけども）
- ちなみにどうあると嬉しいのか
    - プロパティだとinputが外部から読まれる仕組みにならざるを得ない
        - TextFieldからプロパティに書き込むのではなく、ObservableObjectのメソッドを指定して引数付きで呼び出せるのが良い用に思えるが、そんなん考えなくてもプロパティでいいやんという気もする