[Swift]CombineをUIKitで試してみた

## はじめに


iOS開発でリアクティブといえばRxSwift or Combineかと思いますが、自身RxSwiftばかり触っていたのでCombineを触ってみようと単純に思ったのが動機です。

今後SwiftUI＋Combineが主流になった時のことを考え、少しでも慣れておきたいというのも本音です（笑）

さて、今回はTableViewにTodoぽくStringを追加するだけのサンプルです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112697/ab21c0cb-4df4-b5b0-bf01-205e93a2f61a.gif" width="200">

UIKitでCombineを使用しました。

下記がリポジトリです↓

https://github.com/taro-ken/CombineByMVVM-Sample


## ソースコード

```swift: ViewController.swift

import UIKit
import Combine

final class ViewController: UIViewController {
    
    @IBOutlet weak var todoTextField: UITextField!
    @IBOutlet weak var addButton: UIButton!
    
    private let cell = "Cell"
    private let viewModel: ViewModelType = ViewModel()
    private var subscriptions = Set<AnyCancellable>()
      
    
    @IBOutlet weak var todoTableView: UITableView! {
        didSet {
            todoTableView.delegate = self
            todoTableView.dataSource = self
        }
    }
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        addButton.addTarget(self, action: #selector(tappedAddButton), for: .touchUpInside)
        bindOutput()
    }
    
    @objc private func tappedAddButton() {
        viewModel.input.addTodo(text: todoTextField.text)
        todoTextField.text = ""
    }
    
    
    private func bindOutput() {
        viewModel.output.completionSubject.sink {  [weak self] completion in
            self?.todoTableView.reloadData()
        }.store(in: &subscriptions)
        
        viewModel.output.errorSubject.sink { [weak self] error in
            let alert = UIAlertController(title: "エラー", message: .none, preferredStyle: .alert)
            let ok = UIAlertAction(title: "OK", style: .default)
            alert.addAction(ok)
            self?.present(alert, animated: true)
        }.store(in: &subscriptions)
    }
}

extension ViewController: UITableViewDelegate,UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        viewModel.output.todoModel.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
         let cell = tableView.dequeueReusableCell(withIdentifier: cell, for: indexPath)
        cell.textLabel?.text = viewModel.output.todoModel[indexPath.row].title
        return cell
    }
}


```


```swift: ViewModel.swift

import Foundation
import Combine

protocol ViewModelInput {
    func addTodo(text: String?)
}

protocol ViewModelOutput {
    var todoModel: [TodoModel] { get }
    var completionSubject: PassthroughSubject<Void,Never> { get }
    var errorSubject: PassthroughSubject<Void,Never> { get }
}

protocol ViewModelType {
  var input: ViewModelInput { get }
  var output: ViewModelOutput { get }
}

final class ViewModel: ViewModelInput, ViewModelOutput, ViewModelType {
    
    var input: ViewModelInput { return self }
    var output: ViewModelOutput { return self }
    
    // Input
    func addTodo(text: String?) {
        if text == "" {
            errorSubject.send(())
            return
        }
        let todo = TodoModel(title: text)
        todoModel.append(todo)
        completionSubject.send(())
    }
    
    
    // Output
    var todoModel: [TodoModel] = []
    var completionSubject = PassthroughSubject<Void, Never>()
    var errorSubject = PassthroughSubject<Void, Never>()
    
}


```

## 解説

Combineで言うと

- Publishers -> 発行者
- Subscribers -> 購読者

に大きく分けられます。

今回のサンプルの流れとしては

`textFieldに入力`
↓
`viewModelで処理`
↓
`値を流す（発行）`
↓
`Viewで受け取る（購買）`

実にシンプルです

ViewModelの処理を見てみましょう

```swift: ViewModel.swift
protocol ViewModelOutput {
    var todoModel: [TodoModel] { get }
    var completionSubject: PassthroughSubject<Void,Never> { get }
    var errorSubject: PassthroughSubject<Void,Never> { get }
}

```

TabelViewに表示する配列データはViewModelで持ちます。
そして成功した時とエラー時に流すPublishersをそれぞれ定義します。

ここででた`PassthroughSubject`ですが、RxSwiftでいうところの`PublishRelay`にあたり値を保持しないのが特徴です。

逆に`CurrentValueSubject`は値を保持し、RxSwiftの`BehaviorRelay`にあたります。


```swift: ViewController.swift
@objc private func tappedAddButton() {
        viewModel.input.addTodo(text: todoTextField.text)
        todoTextField.text = ""
    }

```
```swift:ViewModel.swift
   // Input
    func addTodo(text: String?) {
        if text == "" {
            errorSubject.send(())
            return
        }
        let todo = TodoModel(title: text)
        todoModel.append(todo)
        completionSubject.send(())
    }

```

ViewController側でタップするとViewModelで処理が走り、
- 空文字だったらerrorSubjectに値（Void）を流す
- 成功したら配列に追加してcompletionSubjectに値（Void）を流す

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

`send`で値を送るの分かりやすいですね👀（RxSwiftでいう`accept`でしょうか）

そしてViewControllerで値を受け取ります。
```swift: ViewController.swift
private func bindOutput() {
        viewModel.output.completionSubject.sink {  [weak self] completion in
            self?.todoTableView.reloadData()
        }.store(in: &subscriptions)
        
        viewModel.output.errorSubject.sink { [weak self] error in
            let alert = UIAlertController(title: "エラー", message: .none, preferredStyle: .alert)
            let ok = UIAlertAction(title: "OK", style: .default)
            alert.addAction(ok)
            self?.present(alert, animated: true)
        }.store(in: &subscriptions)
    }

```
`.sink`でイベントを購買しています（Rxではsubscribe）
成功したらTableViewをリロード、失敗したらアラートを出します。

`.store(in: &subscriptions)`というのがRxSwiftのdisposeBagに相当します。

## おわりに

軽く触ってみて、流れはRxSwiftと感覚的に同じですが覚えることが膨大なので少しずつ慣れていこうと思います！