MVPアーキテクチャについて

#はじめに
今までは、ずっとMVCを採用してアプリを開発してきましたが
MVC脱却を図るため、今回はMVPに関することを備忘録として残しておきます。

まず、MVPを知る前にMVCの流れを復習していきましょう。

#MVC
![MVC画像.001.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/130129b2-e1a3-fba1-971e-d3dd692ccd35.jpeg)

API通信の例で簡単に説明すると、

まず、`View`からユーザーのアクションを検知し、そのアクションを`Controller`に伝えます。
そして、`Controller`は必要な`Model`を取得するためにAPIを叩きます。

その後、APIから必要な`Model`が返され、`Controller`で保持されます。

`Controller`から`View`に`Model`を渡して
その`Model`から`View`を更新することによって画面が更新されます。

#なぜMVCから脱却したいのか？
MVCだと`FatViewController`になりやすいからです。
[iOSDC 2017 前夜祭で「節子、それViewControllerやない…、FatViewControllerや…。」というタイトルで登壇しました！](https://dev.classmethod.jp/articles/iosdc-2017-setsuko/)

余談ですが、上記の記事もMVPについて書かれてました！

#MVP
![MVP画像.001.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/fb9d3a3a-3fcd-86b0-f42f-c8e71a5ffa10.jpeg)

先ほどと同じようにAPI通信の例で説明すると、

まず、`View`からユーザーのアクションを検知します。
そして、`Controller`にアクションを伝えて`Presenter`に知らせます。

`Presenter`は必要な`Model`を取得するためにAPIを叩きます。
その後、APIから必要な`Model`が返され、`Presenter`で保持されます。

`Presenter`から`Controller`に`Model`を渡して
その`Model`を`View`に渡します。

最終的に、`Model`から`View`を更新することによって画面が更新されます。

#Presenterについて
Presenterは、今までMVCで`ViewControllerが行なってきたこと`のほとんどを担っています。
APIを叩いたり、Modelを保持したり等ですね。

そのような責務を引き剥がすことによって`FatViewController`を回避しています。

では実際に、コードで確認していきましょう。

#実装コード
今回は、[Github API](https://docs.github.com/ja/rest)を使用したアプリを例に説明していきます。
![ezgif.com-gif-maker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/743730/c7ca07b0-6dd0-6a81-61e3-74d479c51ff6.gif)

##Presenter
まずは、`Presenter`のコードです。

Presenterの特徴として、

・`UIがどうなっているか`は考慮しない
・`UIKit`をインポートしない
・`Xcode`じゃなくてもコードが書ける

などが挙げられます。

iOS特有の`UIKit`、`SwiftUI`などに依存しないので極端にいうとメモアプリでも書けます。

```swift:MVPSearchPresenter.swift
import Foundation

protocol MVPSearchPresenterInput {
    var numberOfItems: Int { get }
    func item(index: Int) -> GithubModel
    func search(param: String?)
    func didSelect(index: Int)
}

protocol MVPSearchPresenterOutput: AnyObject {
    func update(loading: Bool)
    func update(githubModels: [GithubModel])
    func validation(error: ParameterValidationError)
    func get(error: Error)
    func showWeb(url: URL)
}

final class MVPSearchPresenter {
    private weak var output: MVPSearchPresenterOutput!
    private var api: GithubAPIProtocol!
    private var githubModels: [GithubModel]
    
    init(output: MVPSearchPresenterOutput, api: GithubAPIProtocol = GithubAPI.shared) {
        self.output = output
        self.api = api
        self.githubModels = []
    }
}

extension MVPSearchPresenter: MVPSearchPresenterInput {
    var numberOfItems: Int {
        githubModels.count
    }
    
    func item(index: Int) -> GithubModel {
        githubModels[index]
    }
    
    func search(param: String?) {
        if let validationError = ParameterValidationError(param: param) {
            output.validation(error: validationError)
            return
        }
        guard let searchText = param else { return }
        
        output.update(loading: true)

        self.api.get(searchText: searchText) {[weak self] (result) in
            guard let self = self else{ return }
            switch result {
            case .success(let githubModels):
                self.output.update(loading: false)
                
                if githubModels.isEmpty {
                    self.output.get(error: AppError.emptyApiResponce.error)
                    return
                }
                self.githubModels = githubModels
                self.output.update(githubModels: githubModels)
                
            case .failure(let error):
                self.output.update(loading: false)
                self.output.get(error: error)
            }
        }
    }
    
    func didSelect(index: Int) {
        guard let githubUrl = URL(string: githubModels[index].urlStr) else {
            output.get(error: AppError.getApiData.error)
            return
        }
        output.showWeb(url: githubUrl)
    }
}
```
ではコードを細かく説明していきます。

###・コード説明①
まず、ViewControllerとPresenterを繋げないといけません。

PresenterがViewControllerからの`入力`を受け取り
その結果をViewControllerに`出力`しないといけないからです。

MVPでは`依存性を低く`するために`protocol`によって繋げます。

以下が実際に`protocol`を定義している部分です↓

```swift
// 入力に関するプロトコル
// ViewControllerから送られてくる
protocol MVPSearchPresenterInput {
    var numberOfItems: Int { get }
    func item(index: Int) -> GithubModel
    func search(param: String?)
    func didSelect(index: Int)
}

// 出力に関するプロトコル
// ViewControllerに結果を渡す
protocol MVPSearchPresenterOutput: AnyObject {
    func update(loading: Bool)
    func update(githubModels: [GithubModel])
    func validation(error: ParameterValidationError)
    func get(error: Error)
    func showWeb(url: URL)
}
```
###・コード説明②
Presenterは、`Modelを内部で保持`します。

更に`outputプロパティ`を定義し、`init時にViewControllerと繋げれる`ようにしています。

ここで注意すべき点は、ViewControllerとPresenterがお互いに参照し合うので
`weakキーワード`を付けて`循環参照`にならないようにしないといけません。
[クロージャの中に書く[weak self]についてまとめてみた](https://qiita.com/reo0612/items/b0d1ee00ebf3a0e7c857)

```swift
final class MVPSearchPresenter {
    // ViewControllerとPresenterが参照し合い循環参照が起きるためweakキーワードを付ける
    // このoutputがViewControllerのこと
    private weak var output: MVPSearchPresenterOutput!
    private var api: GithubAPIProtocol!
    // Modelを保持する
    private var githubModels: [GithubModel]
    
    init(output: MVPSearchPresenterOutput, api: GithubAPIProtocol = GithubAPI.shared) {
        self.output = output
        self.api = api
        self.githubModels = []
    }
}
```
###・コード説明③
`MVPSearchPresenterInputプロトコル`を準拠して`ViewControllerからの入力を処理`します。

Presenterとしては、何でか分からないけど
とりあえず`入力`が来たから処理して`出力`したって感じです。

Presenterにとっては、検索バーがタップされたとか画面が遷移したとかは`関係なく知らなくても良い`のです。
更に、入力・出力する相手も関係ないので`テストがしやすくデザインを決める前にも開発が可能`です。

```swift
extension MVPSearchPresenter: MVPSearchPresenterInput {
    var numberOfItems: Int {
        githubModels.count
    }
    
    func item(index: Int) -> GithubModel {
        githubModels[index]
    }
    
    func search(param: String?) {
        if let validationError = ParameterValidationError(param: param) {
            // ViewControllerに任せる
            output.validation(error: validationError)
            return
        }
        guard let searchText = param else { return }
        // ViewControllerに任せる
        output.update(loading: true)
        // API通信
        self.api.get(searchText: searchText) {[weak self] (result) in
            guard let self = self else{ return }
            switch result {
            case .success(let githubModels):
                // ViewControllerに任せる
                self.output.update(loading: false)
                
                if githubModels.isEmpty {
                    // ViewControllerに任せる
                    self.output.get(error: AppError.emptyApiResponce.error)
                    return
                }
                self.githubModels = githubModels
                // ViewControllerに任せる
                self.output.update(githubModels: githubModels)
                
            case .failure(let error):
                // ViewControllerに任せる
                self.output.update(loading: false)
                self.output.get(error: error)
            }
        }
    }
    
    func didSelect(index: Int) {
        guard let githubUrl = URL(string: githubModels[index].urlStr) else {
            // ViewControllerに任せる
            output.get(error: AppError.getApiData.error)
            return
        }
        // ViewControllerに任せる
        output.showWeb(url: githubUrl)
    }
}
```

##ViewController

では、ViewControllerのコードです。

ViewControllerの特徴として、

・`Viewに関すること以外`は書かない
・`if`や`for`等といった制御構文が入らない

などが挙げられます。

```swift:MVPSearchViewController.swift
import UIKit

final class MVPSearchViewController: UIViewController {
    @IBOutlet weak private var tableView: UITableView! {
        didSet {
            tableView.register(UINib(nibName: TableViewCell.className, bundle: nil), forCellReuseIdentifier: TableViewCell.className)
        }
    }
    @IBOutlet weak private var indicator: UIActivityIndicatorView!
    
    private var searchBar = UISearchBar()

    private var input: MVPSearchPresenterInput!
    func inject(input: MVPSearchPresenterInput) {
        self.input = input
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        self.navigationItem.titleView = searchBar
        searchBar.delegate = self
    }
}

extension MVPSearchViewController: UISearchBarDelegate {
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        input.search(param: searchBar.text)
        searchBar.resignFirstResponder()
    }
}

extension MVPSearchViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        input.numberOfItems
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.className, for: indexPath) as! TableViewCell
        let githubModel = input.item(index: indexPath.item)
        cell.configure(githubModel: githubModel)
        return cell
    }
}

extension MVPSearchViewController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        input.didSelect(index: indexPath.row)
    }
}

extension MVPSearchViewController: MVPSearchPresenterOutput {
    func update(loading: Bool) {
        indicator.animation(isStart: loading)
    }
    
    func update(githubModels: [GithubModel]) {
        DispatchQueue.main.async {
            self.searchBar.text = ""
            self.searchBar.resignFirstResponder()
            self.tableView.reloadData()
        }
    }
    
    func validation(error: ParameterValidationError) {
        Alert.okAlert(vc: self, title: error.message, message: "")
    }
    
    func get(error: Error) {
        Alert.okAlert(vc: self, title: error.localizedDescription, message: "")
    }
    
    func showWeb(url: URL) {
        Router.showWeb(url: url, from: self)
    }
}
```
では細かく説明していきます。

###・コード説明①
まず、Presenterと繋げるために`inputプロパティ`と`injectメソッド`を用意します。
この部分で外部からPresenterを繋げます。

```swift
    // このinputがpresenterのこと
    private var input: MVPSearchPresenterInput!
    // ここで外部からPresenterを繋げる
    func inject(input: MVPSearchPresenterInput) {
        self.input = input
    }
```
###・コード説明②
今回は、キーボードの検索ボタンを押した時にAPI通信を行い
その結果をTableViewに表示するといったアプリとなっています。

なので検索ボタンを押した時に`Presenterに知らせないといけません`。

```swift
extension MVPSearchViewController: UISearchBarDelegate {
    func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
        // Presenterに知らせる
        input.search(param: searchBar.text)
        searchBar.resignFirstResponder()
    }
}
```
###・コード説明③
Presenterからの結果を`ViewControllerが受け取らないといけません`。

なので、`MVPSearchPresenterOutputプロトコル`を準拠して
以下のように結果を受け取れるようにします。

```swift
extension MVPSearchViewController: MVPSearchPresenterOutput {
    func update(loading: Bool) {
        // インディケータを回すかどうかを決めている
        indicator.animation(isStart: loading)
    }
    
    func update(githubModels: [GithubModel]) {
        // TableViewの更新など
        DispatchQueue.main.async {
            self.searchBar.text = ""
            self.searchBar.resignFirstResponder()
            self.tableView.reloadData()
        }
    }
    
    func validation(error: ParameterValidationError) {
        // アラート表示
        Alert.okAlert(vc: self, title: error.message, message: "")
    }
    
    func get(error: Error) {
        Alert.okAlert(vc: self, title: error.localizedDescription, message: "")
    }
    
    func showWeb(url: URL) {
        // 画面遷移する
        Router.showWeb(url: url, from: self)
    }
}
```
#PresenterとViewControllerを繋げる
PresenterとViewControllerを繋げる準備はしましたが、このままだとまだ繋がっていません。

ではどこで繋げていくかというと、`画面遷移に関するクラス`にて繋げています。
[画面遷移に関係あるコードの記述を別ファイルに分けて実装する](https://qiita.com/reo0612/items/4235bdf51c3dadd256cb)

```swift:Router.swift
final class Router {
    static func showMVPSearch(from: UIViewController) {
        let mvpSearchVC = UIStoryboard.mvpSearchViewController
        // ここでPresenterとViewControllerを繋げている
        let presenter = MVPSearchPresenter(output: mvpSearchVC)
        mvpSearchVC.inject(input: presenter)
        from.show(next: mvpSearchVC)
    }
}
```
#MVPを採用してみて
単純に`ViewControllerの記述量が減った`のが良きですね。

今回のサンプルアプリでも、その恩恵を得られたので
大規模なアプリだと、もっと効果を感じれそうです。

PresenterはViewがどうなろうと関係ないのでアプリの仕様が分かっていれば
`すぐに開発できる`メリットもありますし、何なら`Xcode`にも依存していません。

後は、入力・出力先に依存していないので`テストしやすい`です。
何か不明な不具合があった時にでも、Presenterは入力・出力先を変えてテスト可能です。

#おわりに
ソースコードは[こちら](https://github.com/reo0612/Architecture-Sample)にあげてます。

何かあれば、コメントして下さると有り難いです。