CombineとVIPERでアプリを構築する

## はじめに
iOS13以降、Frameworkとして提供されたCombine。
上記のおかげで外部ライブラリに頼らずにリアクティブプログラミングを使うことができるようになり、アプリを構築する上での選択肢の一つとして考えられている方も多いのではないでしょうか？
では、実際にアプリを作る上でどういった形でCombineを使っていくことになるか...
ということで、今回はCombineとVIPERを使って簡単なアプリ作ってみましたので、作ったアプリを元に処理の流れを記載していきます。

## CombineとVIPERとは
まずは、CombineとVIPERについて軽く紹介したいと思います。

*※ CombineとVIPERの詳細については、今回の本筋から逸れるため、詳細な説明はいたしません。
詳しくは知りたい方は別記事をご参照いただきますようお願いいたします。*

### Combineとは
イベントをどのように処理するかを宣言的に記述することができるAPIです。
関数型リアクティブプログラミング（FRP）のフレームワークであり、FRPだと他にはRxSwiftといった外部ライブラリが有名です。
CombineはAppleが提供しているため外部ライブラリを別途追加する必要がないところが嬉しいところです。

[Combine | Apple Developer Documentation](https://developer.apple.com/documentation/combine)

### VIPERとは
Clean ArchitectureをiOSアプリに特化した構成に変更したアーキテクチャです。
View, Interactor, Presenter, Router, Entityの5つの要素で構成され、それぞれの頭文字を取ってVIPERという名がついています。

|要素名|役割|
|:---|:---|
|View|画面の表示、更新を実施<br>ユーザーアクションが来たらPresnterに伝える|
|Presenter|Viewから渡されたイベントを元に、InteractorやRouterに処理の依頼を投げるハブとしての役割を担当|
|Interactor|Presenterからの依頼を元にビジネスロジックを実施|
|Router|画面遷移を実施<br>また、遷移時にViewに必要な値を注入するDIを行う|
|Entity|データ構造を定義|

VIPERの目的の一つが**「依存性を分離」**することであり、そのために各要素はそれぞれ必ずprotocolを介してアクセスすることとなっています。
要素の切り替えを容易になったことで簡単にテストコードが記述できます。
また、もう一つの目的として**「単一責任の原則」**を遵守することがあり、上記で挙げた5つの要素にはその役割を超えた処理は書きません。
上記を守ることで、責務を切り離して検証を容易にします。

[iOS Project Architecture: Using VIPER | Cheesecake Labs](https://cheesecakelabs.com/blog/ios-project-architecture-using-viper/)

## CombineとVIPERでアプリを構築
では、本題に入っていきましょう。
今回はGitHub APIを叩いてユーザー一覧を取得する処理の流れを追いながらコードを見ていきます。

参考アプリはGitHubに上げておりますので、こちらも合わせてご参照下さい。
[ViperCombineExampleApp](https://github.com/k-awoki/ViperCombineExampleApp)

### Interactor
`APIClient`にユーザー一覧をリクエストし、レスポンスを`AnyPublisher`で返却するようにしています。
`.eraseToAnyPublisher() `を使って`AnyPublisher`に型を変換しています。

```swift:UserListInteractor.swift
import Combine

protocol UserListInteractorUseCase: AnyObject {
    func fetch() -> AnyPublisher<[User], Error>
}

final class UserListInteractor {
    private let resource = GitHubUsesApiResource()
}

// MARK: - UserListInteractorUseCase
extension UserListInteractor: UserListInteractorUseCase {
    func fetch() -> AnyPublisher<[User], Error> {
        return Future<[User], Error> { [weak self] promise in
            guard let resource = self?.resource else {
                fatalError()
            }
            ApiClinet.request(resource, completion: { result in
                switch result {
                case .success(let response):
                    promise(.success(response))
                case .failure(let error):
                    promise(.failure(error))
                }
            })
        }
        .eraseToAnyPublisher()
    }
}
```

### Presenter
`Presenter`では`View`と`Interactor`、及び`Router`の橋渡しを行います。
`input`に対して`View`からイベント通知が飛んできますので、`input`のイベント発火をトリガーとして`Interactor`、または`Router`に処理を依頼します。
`Interactor`からレスポンスが帰ってきた場合は、`output`にデータを流し込むことで`View`側に更新を知らせます。

```swift:UserListPresenter.swift
protocol UserListPresenterInput: AnyObject {
    var viewDidLoadTrigger: PassthroughSubject<Void, Never> { get }
    var didSelectUserTrigger: PassthroughSubject<User, Never> { get }
}
protocol UserListPresenterOutput: AnyObject {
    var users: CurrentValueSubject<[User], Error> { get }
}
protocol UserListPresenterInterface {
    var input: UserListPresenterInput? { get }
    var output: UserListPresenterOutput? { get }
}

final class UserListPresenter: UserListPresenterInterface, UserListPresenterInput, UserListPresenterOutput {
    // MARK: - Inputs
    var viewDidLoadTrigger = PassthroughSubject<Void, Never>()
    var didSelectUserTrigger = PassthroughSubject<User, Never>()

    // MARK: - Outputs
    var users = CurrentValueSubject<[User], Error>([])

    // MARK: - Constants
    private let interactor: UserListInteractorUseCase
    private let router: UserListWireframe

    // MARK: - Variables
    weak var input: UserListPresenterInput? { return self 
    weak var output: UserListPresenterOutput? { return self }
    private var cancellables = [AnyCancellable]()

    // MARK: - Lifecycle Methods
    init(interactor: UserListInteractorUseCase,
         router: UserListWireframe) {
        self.interactor = interactor
        self.router = router
        bind()
    }
}

// MARK: - Private Methods
private extension UserListPresenter {
    private func bind() {
        // interactorがnilになる可能性があるので、一度compactMapでnilを除外した後に処理を実行
        viewDidLoadTrigger
            .compactMap({ [weak self] in self?.interactor.fetch() })
            .flatMap({ $0 })
            .sink(receiveCompletion: { [weak self] completion in
                if case .failure(let error) = completion {
                    self?.users.send(completion: .failure(error))
                }
            }, receiveValue: { [weak self] value in
                self?.users.send(value)
            })
            .store(in: &cancellables)

        didSelectUserTrigger
            .sink(receiveValue: { [weak self] user in
                self?.router.showRepositoryList(user)
            })
            .store(in: &cancellables)
    }
}
```

### View
`Lifecycle`やユーザーからのアクションを受け取ったタイミングで`presenter`に`input`を通して更新依頼を投げます。
そして、`output`で値の更新を受け取り、各`View`の表示内容の更新を行います。

```swift:UserListViewController.swift
class UserListViewController: UIViewController {

    // MARK: - Variables
    var presenter: UserListPresenterInterface!
    private var users: [User] = []
    private var cancellables = [AnyCancellable]()

    // MARK: - Outlets
    @IBOutlet private weak var tableView: UITableView!

    // MARK: - Lifecycle Metohds
    override func viewDidLoad() {
        super.viewDidLoad()
        bind()
        presenter.input?.viewDidLoadTrigger.send(())
    }
}

// MARK: - Priavte Methods
private extension UserListViewController {
    /// bind
    func bind() {
        // UIを更新するのでmainスレッドを指定する
        presenter.output?.users
            .receive(on: DispatchQueue.main)
            .sink(receiveCompletion: { completion in
                if case .failure(let error) = completion {
                    print("error: \(error)")
                }
            }, receiveValue: { [weak self] value in
                self?.users = value
                self?.tableView.reloadData()
            })
            .store(in: &cancellables)
    }
}

// MARK: - UITableViewDataSource
extension UserListViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return users.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "UserCell", for: indexPath) as? UserCell else {
            return UITableViewCell()
        }
        cell.configure(with: users[indexPath.row])
        return cell
    }
}

// MARK: - UITableViewDelegate
extension UserListViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        presenter.input?.didSelectUserTrigger.send(users[indexPath.row])
    }
}
```

#### アプリを作ってみて
Combineを使用すること処理の流れが追いやすくなっているかと思います。
また、一つのトリガーに対して複数の処理を記載する場合に、処理を分けて記載することができるので余計な分岐などが減ってより見やすくなるなると思います。
もちろん、RxSwiftでも同等のことができるかと思いますが、純正APIであること、また、今後SwiftUIを使っていく場合はCombineがより強力な武器になっていくと思いまいますので、ぜひ皆様もお試しいただけると良いかと思います。

### 最後に
改めてにはなりますが、GitHubにサンプルアプリを載せていますので、気になった方はこちらも合わせてご参照いただけますと幸いです。
[ViperCombineExampleApp](https://github.com/k-awoki/ViperCombineExampleApp)

また、何か、改善案やご意見等ありましたら、コメントいただけますと幸いです。
どうぞよろしくお願いいたします。