UIKitでもCombineを使いたい。

## はじめに
iOS14がリリースされ、Swft自体のアップデートやSwiftUIのスタンダード化が進む昨今ですが、
iOS12は切れなくて、実務ではCombineを使えていない方も多いのではないでしょうか。

けど新しい技術試してみたいですよね。
SwiftUI+Combineはちょっとハードル高いけど、まずは慣れたUIKitでCombineだけでも試してみたいですよね！！

というわけで今回は以下の内容で記事を書いてみました。

* Combineの基礎概念について
* UIKitで使ってみる

お役にたてば幸いです。:bow:

※ とりあえずコードが見たいって場合は以下をご覧いただけると幸いです。:bow:
[toya108/CombineBookManagerApp](https://github.com/toya108/CombineBookManagerApp)

## 環境
この記事は以下の動作環境で動作確認しています。

* Xcode12
* Swift5.3

# Combineって何？
一言で表すと`Apple純正の非同期フレームワーク`です。
AppleDeveloperの説明を引用します。

> The Combine framework provides a declarative Swift API for processing values over time.

「Combineフレームワークは時間の経過に応じて値を処理するための宣言型のAPIを提供します。」みたいな意味です。

* 宣言的に書ける
* 時間の経過に応じて値を処理する

あたりがCombineの主機能であり、非同期フレームワークの性質でもありそうです。

## Combineの登場人物を知る

Combineを使っててみる前に、全体の流れとCombineを使う上で知っておかなければいけない登場人物を紹介します。

### 登場人物
* Publisher: 値を発行する。
* Operator: 発行した値を変換する。
* Subscriber: 値を受け取ってイベントを発火する。

図にするとこんな感じ。

![Untitled Diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311282/f43d35e4-1b10-8d9b-1b7f-21ccf8a38a88.png)


Rxやリアクティブプログラミングでは上記の概念をよくマーブルダイアグラムで表現しますが、
個人的には上の図のように登場人物を1-1-1で出した図で概念を捉えた方が頭に入りやすいです。

## とりあえず触ってみよう
登場人物が分かったのでとりあえず触って見ましょう。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311282/505cfd1d-f3c1-b681-ff8f-ded129a423ec.png" width="250">

よく見るログイン画面です。
MVVMで設計している場合、ViewでTextFieldから受け取ったメールアドレスをViewModelにバインドする必要があります。
その時の実装を例にして、Publisher、Operator、Subscriberの解説と実装例を見ていきましょう。

### Publisher
まずはPublisherです。
https://developer.apple.com/documentation/combine/publisher

Publisherは値を発行できるすごいやつなので、Viewで受け取ったメールアドレスを発行することができます。

まずはTextFieldのtextに変更があったら値を発行するPublisherを作って見ましょう。

```swift
import Combine

final class LoginViewController: UIViewController {

    @IBOutlet weak var mailAddressTextField: UITextField!

    private let viewModel = LoginViewModel()
    private var binding = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // textDidChangeNotificationが通知されたら`mailAddressTextField`というobjectを発行する。
        NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: mailAddressTextField)

    }
}
```

importも忘れないようにしましょう。
これでtextFieldに変更があった時に`mailAddressTextField`というobjectが発行されるようになります。

![Untitled Diagram (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311282/59b59556-cef1-07b2-f43b-236eb8d09582.png)

### Operator
続いてOperatorいきましょう。
Operatorは値を変換できるすごいやつなので、上でPublisherが発行したUITextFieldのobjectも変換できちゃいます。
TextFieldの値のViewModelにバインドする時に、ViewModelに渡すのはただのStringで十分です。
なので、Operatorを使ってUITextFieldをStringに変換して見ましょう。

```swift
import Combine

final class LoginViewController: UIViewController {

    @IBOutlet weak var mailAddressTextField: UITextField!

    private let viewModel = LoginViewModel()
    private var binding = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: mailAddressTextField)
            .compactMap { $0.object as? UITextField } // UITextFieldのキャストに失敗してnilになったら弾く
            .map { $0.text ?? "" } // UITextFieldからtextを取り出してStringに変換
    }
}
```

* `.compactMap`でUITextField以外のインスタンスを排除。
* `.map`でUITextFieldからtextを取得し、Stringに変換。

ということをやっています。

![Untitled Diagram (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311282/271e5bba-a7d3-a25b-2807-550910d43c57.png)


このように、Publisherが発行した値と受け取りたい値が異なる時に、その中間で値の変換やハンドリングをしてくれるのがOperatorです。
Operatorの種類については以下の参照してください。
* [【iOS】Combineフレームワークまとめ](https://qiita.com/shiz/items/5efac86479db77a52ccc#operators)

### Subscriber
最後はSbscriberです。
Sbscriberは値を受け取って色々できるすごいやつなので、Publisherから流れてきたメールアドレスのStringをViewModelにセットできます。

```swift
import Combine

final class LoginViewController: UIViewController {

    @IBOutlet weak var mailAddressTextField: UITextField!
    
    private let viewModel = LoginViewModel()
    private var binding = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        NotificationCenter.default
            .publisher(for: UITextField.textDidChangeNotification, object: mailAddressTextField)
            .compactMap { $0.object as? UITextField }
            .map { $0.text ?? "" }
            .removeDuplicates() // 重複した値は排除
            .eraseToAnyPublisher()
            .receive(on: RunLoop.main)
            .assign(to: \.mailAddress, on: viewModel) // viewModelの\.mailAddressに値を代入
            .store(in: &binding)
    }
}

final class LoginViewModel {
    var mailAddress: String = "" {
        didSet {
            print(mailAddress)
        }
    }
}
```

色々やっているように見えますが、ポイントは2つです。
* `.assign(to: \.mailAddress, on: viewModel)`で受け取ったStringをViewModelにセットしている。
* `.store(in: &binding)`でインスタンスを監視キャンセル可能にしている。

assignメソッドはPublisherから生えており、受け取った値をkeypathで定義されたオブジェクトへ渡しつつSubscriberを生成できます。
ただそのままだとメモリが解放されないため、よしなに監視をキャンセルされるように`.store(in: &binding)`を呼んでいます。

![Untitled Diagram (3).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311282/8d55ef25-6539-f00c-5404-125b889cd1c2.png)

※この辺は曖昧な理解で説明できないと思ったので、詳しくは@shizさんの記事を参照してください。
[【iOS】Combineフレームワークまとめ](https://qiita.com/shiz/items/5efac86479db77a52ccc#subscriber)

## 実行

ここまでのコードでTextFieldの値を受け取ったらViewModelにセットしてくれるようになりました！
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311282/c61101a1-9470-c93e-bdcb-ee04a2a0a500.png" width="250">
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/311282/aeaef8bc-3e0b-026a-04fd-47032277c4e0.png" width="500">

## まとめ
CombineはApple純正の非同期フレームワークであり、以下の特徴がある。

* 宣言的に書ける
* 時間の経過に応じて値を処理する

Combineには以下の主要な登場人物がいる。

* Publisher: 値を発行する。
* Operator: 発行した値を変換する。
* Subscriber: 値を受け取ってイベントを発火する。

## おまけ
Combineに慣れるためにUIKitとCombineでAPI叩いて書籍管理をするアプリも作ってます。
Combineを組み込んだAPIクライアントやExtension化してより使いやすくしたPublisher等が載っているつもりなので、こちらもよければご覧ください:bow:

[toya108/CombineBookManagerApp](https://github.com/toya108/CombineBookManagerApp)