RxSwiftのbind関数をCombineで実装してみた

今までは[RxSwift](https://github.com/ReactiveX/RxSwift/tree/5.0.1)を用いて非同期に値の変化を監視して処理を行う実装をしてきましたが、iOS13から[Combine Framework](https://developer.apple.com/documentation/combine)が使えるようになり同等の処理が行えるようになったので現在勉強をしています。

勉強していく中で、`RxSwift`の[Relay](https://github.com/ReactiveX/RxSwift/blob/5.0.1/Documentation/Subjects.md#relays)、[bind関数](https://github.com/ReactiveX/RxSwift/blob/5.0.1/RxRelay/Observable%2BBind.swift)を作ってみたので今回紹介します。

## 検証環境

- Xcode 11.2.1
- Swift 5.1
- RxSwift 5.0.1

## 背景

元々`RxSwift`の`bind関数`を利用していたコードを`Combine`で書き換えた所、`RxSwift`の`bind関数`の方が読みやすい(その書き方に慣れているせいか）と感じたので、【`Combine`でも同じ形式で呼び出せるようできないか？】というのがきっかけとなります。

ではまず`RxSwift`で実装した例を元に、`Combine`を使った書き方に書き直してみましょう。

### 具体例

`姓`と`名`を入力する２つの入力フォームがあり、登録ボタンをタップすると入力した文字列を出力するコードを書いていきます。
※bind関数を使った例を書きたいので、登録ボタンタップ時に`UITextField`インスタンスの`text`プロパティを参照すればいいじゃんというツッコミはお控えください。:bow:

#### RxSwiftで書いた場合

以下のように記載できます。
各入力フォームのテキストを表す`ControlProperty`の値が`bind(to:)関数`で対応する`Relay`にバインドされるという直感的なコードです。

```swift
import RxCocoa
import RxRelay
import RxSwift
import UIKit

struct NameRegisterInputData {
  let firstNameTextRelay = BehaviorRelay<String>(value: "")
  let lastNameTextRelay = BehaviorRelay<String>(value: "")
}

final class NameRegisterViewController: UIViewController {
  @IBOutlet private var firstNameTextField: UITextField!
  @IBOutlet private var lastNameTextField: UITextField!

  private let input = NameRegisterInputData()
  private let disposeBag = DisposeBag()

  override func viewDidLoad() {
    super.viewDidLoad()
    setupDataBinding()
  }

  private func setupDataBinding() {
    let firstNameTextControlProperty = firstNameTextField.rx.textInput.text.orEmpty
    let lastNameTextControlProperty = lastNameTextField.rx.textInput.text.orEmpty
    
    firstNameTextControlProperty.bind(to: input.firstNameTextRelay).disposed(by: disposeBag)
    lastNameTextControlProperty.bind(to: input.lastNameTextRelay).disposed(by: disposeBag)
  }

  @IBAction private func tapRegisterButton() {
    print("Input Data: \(input.firstNameTextRelay.value) \(input.lastNameTextRelay.value)")
  }
}
```

#### Combineで書いた場合

`Combine`でbind処理を実現する場合には[`assign(to:on:)関数`](https://developer.apple.com/documentation/combine/publisher/3235801-assign)を利用します。
この関数では、更新するインスタンスを第二引数として指定し、そのインタンスの更新したいプロパティを`KeyPath`として第一引数に指定します。

```swift
import Combine
import UIKit

class NameRegisterInputData {
  var firstNameText = ""
  var lastNameText = ""
}

final class NameRegisterViewController: UIViewController {
  @IBOutlet private var firstNameTextField: UITextField!
  @IBOutlet private var lastNameTextField: UITextField!

  private let input = NameRegisterInputData()
  private var cancellables: Set<AnyCancellable> = []

  override func viewDidLoad() {
    super.viewDidLoad()
    setupDataBinding()
  }

  private func setupDataBinding() {
    let firstNameTextPublisher = firstNameTextField.textPublisher.orEmpty
    let lastNameTextPublisher = lastNameTextField.textPublisher.orEmpty
    
    firstNameTextPublisher.assign(to: \.firstNameText, on: input).store(in: &cancellables)
    lastNameTextPublisher.assign(to: \.lastNameText, on: input).store(in: &cancellables)
  }

  @IBAction private func tapRegisterButton() {
    print("Input Data: \(input.firstNameText) \(input.lastNameText)")
  }
}

// 以下は、UITextFieldの入力文字列を監視するためのExtension
extension UITextField {
  var textPublisher: AnyPublisher<String?, Never> {
    NotificationCenter.default
      .publisher(for: UITextField.textDidChangeNotification, object: self)
      .map { $0.object as! UITextField }
      .map { $0.text }
      .prepend(text)
      .eraseToAnyPublisher()
  }
}

extension AnyPublisher where Output == String?, Failure == Never {
  var orEmpty: AnyPublisher<String, Never> {
    map { $0 ?? "" }.eraseToAnyPublisher()
  }
}
```

`assign(to:on:)関数`を用いた記述がまどろっこしく感じたので、`Combine`でも`RxSwift`のように`bind(to:)関数`のように呼び出せるようにしていきます。

## コードリーディング

Combine版の`Relay,bind関数`を実装する前に、`RxSwift`のコードを読んでどんな機能を提供しているのか改めて確認してみましょう。

### RxSwiftのbind関数

[bind関数](https://github.com/ReactiveX/RxSwift/blob/5.0.1/RxRelay/Observable%2BBind.swift)は、`ObservableType`の拡張関数として定義され、引数として`PublishRelay`または`BehaviorRelay`型のインスタンスを指定し、`Disposable`型のインスタンス変数を返却していることが分かります。

`RxSwift`の[ObservableType](https://github.com/ReactiveX/RxSwift/blob/5.0.1/RxSwift/ObservableType.swift)はプッシュ型のシーケンスを表します。これは`Combine`の[Publisher](https://developer.apple.com/documentation/combine/publisher)が該当します。
同様に[Disposable](https://github.com/ReactiveX/RxSwift/blob/5.0.1/RxSwift/Disposable.swift)は`dispose関数`が定義され、この関数を呼び出す事で現在購読中の処理を終了する事ができます。こちらは、`Combine`の[Cancellable](https://developer.apple.com/documentation/combine/cancellable)が似たような立ち位置で存在します。

### RxSwiftのRelay

しかし、`PublishRelay`または`BehaviorRelay`に対応する定義は`Combine`に現在ありません。
なので、`Relay`についても`RxSwift`のコードを読んでみましょう。

[PublishRelay](https://github.com/ReactiveX/RxSwift/blob/5.0.1/RxRelay/PublishRelay.swift)、[BehaviorRelay](https://github.com/ReactiveX/RxSwift/blob/5.0.1/RxRelay/BehaviorRelay.swift)ともに`Subject`をラップしている事が分かります。
`Subject`の`onEvent関数`を内部的に呼び出す`accept関数`の実装もしています。

また、`BehaviorRelay`に関しては、`BehaviorSubject`と同様に購読時に現在の値を受け取れる性質があるため、イニシャライザとして初期値を設定できるようになっています。
現在値もComputed Propertyとして定義され、取得できるようです。

`Combine`では`PublishSubject`、`BehaviorSubject`に対応する型として、[PassthroughSubject](https://developer.apple.com/documentation/combine/passthroughsubject)、[CurrentValueSubject](https://developer.apple.com/documentation/combine/currentvaluesubject)という型が定義されています。

これらの情報を用いて実装できそうですね！:grinning:
次の章では実際にコードリーディングした内容を元に実装をしていきます。

## 実装

### Relay

まず`Relay`プロトコルを作ります。
`Publisher`プロトコルを継承しますが、エラーイベントは発生させないので`Failure`には`Never`という条件を設定します。
`RxSwift`同様に`accept`関数を定義しました。

```swift:Relay.swift
import Combine

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public protocol Relay: Publisher where Failure == Never {
  func accept(_ event: Self.Output)
}
```

続いて`PassthroughRelay`の実装です。
※CombineのSubject名に合わせて`PublishRelay`ではなく`PassthroughRelay`としました。

先程の`Relay`プロトコルに適合させ、`RxSwift`同様に`PassthroughSubject`のインスタンスを内部で保持します。
あとは`accept関数`を実装しつつ、`Publisher`プロトコルに適合させます。

```swift:PassthroughRelay.swift
import Combine

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class PassthroughRelay<Output>: Relay {
  private let subject = PassthroughSubject<Output, Never>()

  public init() {}

  public func accept(_ event: Output) {
    subject.send(event)
  }

  public func receive<S>(subscriber: S) where S: Subscriber, S.Failure == Never, Output == S.Input {
    subject.receive(subscriber: subscriber)
  }
}
```

続いて`CurrentValueRelay`の実装です。
※CombineのSubject名に合わせて`BehaviorRelay`ではなく`CurrentValueRelay`としました。

先程の`Relay`プロトコルに適合させ、`RxSwift`同様に`CurrentValueSubject`のインスタンスを内部で保持します。
`PassthroughSubject`同様の実装をしつつ、初期値が設定できるイニシャライザの定義と、現在値を取得することができるComputed Propertyの定義も行いました。

```swift:CurrentValueRelay.swift
import Combine

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class CurrentValueRelay<Output>: Relay {
  private let subject: CurrentValueSubject<Output, Never>

  public var value: Output {
    subject.value
  }

  public init(_ value: Output) {
    subject = CurrentValueSubject(value)
  }

  public func accept(_ event: Output) {
    subject.send(event)
  }

  public func receive<S>(subscriber: S) where S: Subscriber, S.Failure == Never, Output == S.Input {
    subject.receive(subscriber: subscriber)
  }
}
```

### bind関数

作成した`PassthroughSubject`と`CurrentValueRelay`を利用して`bind関数`を実装していきます。
`RxSwift`の`ObservableType`に対応するものが`Combine`の`Publisher`であるため、`Publisher`の`extension`として実装していきます。
また、エラーイベントを`bind関数`では受け取らないようにするために、`Failure`には`Never`という条件を設定しました。

実装の実体については`RxSwift`の実装を参考に実装しています。

```swift:Publisher+Bind.swift
import Combine

@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public extension Publisher where Self.Failure == Never {
  func bind(to relays: CurrentValueRelay<Output>...) -> AnyCancellable {
    bind(to: relays)
  }

  func bind(to relays: CurrentValueRelay<Output?>...) -> AnyCancellable {
    map { $0 as Output? }.bind(to: relays)
  }

  private func bind(to relays: [CurrentValueRelay<Output>]) -> AnyCancellable {
    sink { value in
      relays.forEach { $0.accept(value) }
    }
  }

  func bind(to relays: PassthroughRelay<Output>...) -> AnyCancellable {
    bind(to: relays)
  }

  func bind(to relays: PassthroughRelay<Output?>...) -> AnyCancellable {
    map { $0 as Output? }.bind(to: relays)
  }

  private func bind(to relays: [PassthroughRelay<Output>]) -> AnyCancellable {
    sink { value in
      relays.forEach { $0.accept(value) }
    }
  }
}
```

## 使ってみる

開発した`Combine版Relay`を使って先程の`assign(to:on:)関数`を`bind(to:)`関数に変更してみました。
すると、[RxSwiftで書いた場合](#rxswiftで書いた場合)のようにより直感的なコードで記述できるようになりました。

```swift
import Combine
import UIKit

struct NameRegisterInputData {
  let firstNameTextRelay = CurrentValueRelay<String>("")
  let lastNameTextRelay = CurrentValueRelay<String>("")
}

final class NameRegisterViewController: UIViewController {
  @IBOutlet private var firstNameTextField: UITextField!
  @IBOutlet private var lastNameTextField: UITextField!

  private let input = NameRegisterInputData()
  private var cancellables: Set<AnyCancellable> = []

  override func viewDidLoad() {
    super.viewDidLoad()
    setupDataBinding()
  }

  private func setupDataBinding() {
    let firstNameTextPublisher = firstNameTextField.textPublisher.orEmpty
    let lastNameTextPublisher = lastNameTextField.textPublisher.orEmpty

    firstNameTextPublisher.bind(to: input.firstNameTextRelay).store(in: &cancellables)
    lastNameTextPublisher.bind(to: input.lastNameTextRelay).store(in: &cancellables)
  }

  @IBAction private func tapRegisterButton() {
    print("Input Data: \(input.firstNameTextRelay.value) \(input.lastNameTextRelay.value)")
  }
}

// 以下は、UITextFieldの入力文字列を監視するためのExtension
extension UITextField {
  var textPublisher: AnyPublisher<String?, Never> {
    NotificationCenter.default
      .publisher(for: UITextField.textDidChangeNotification, object: self)
      .map { $0.object as! UITextField }
      .map { $0.text }
      .prepend(text)
      .eraseToAnyPublisher()
  }
}

extension AnyPublisher where Output == String?, Failure == Never {
  var orEmpty: AnyPublisher<String, Never> {
    map { $0 ?? "" }.eraseToAnyPublisher()
  }
}
```

## まとめ

今回`bind関数`を`Combine`で実装するために、`RxSwift`のコードリーディングを行いました。
`RxSwift`でどのように`bind関数・Relay`の実装がされているのかを知る機会となり、非常に面白かったです。

また、現状は`NotificationCenter`で`Combine`の機能が利用できるものの`RxCocoa`ほどUIイベントのハンドリングは行えないので、`RxCocoa`のコードリーディングをしながらUIイベントのハンドリングもより簡単に行えるように実装をできればと考えています。

明日は@sadashiさんの「Android View Bind Library 比較」です。お楽しみに！