MVPアーキテクチャでGitHubAPIを叩く

MVPアーキテクチャを勉強中です。

今回はGitHubAPIを叩いてTableViewに表示するサンプルを
MVPを用いて作成します。

自身の備忘録として残しておきます。

まず、オーソドックスなMVCアーキテクチャをおさらい。

##MVC（Model,View,Controller）
![MVC-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112697/20a6bd8d-1be5-cd73-2c90-a5fb327e81b8.jpeg)


※API通信の例で簡単に説明します。

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

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

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

##MVCからMVPに移行する理由

結論、MVCはViewControllerに処理を詰め込みすぎて肥大し
俗に言うFatViewControllerになりやすいからです。

##MVP(Model,View,Presenter）

![MVP-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112697/9f198a5f-b368-53dc-ce5e-f6cd4116827b.jpeg)

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

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

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

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

##Presenterとは？

Presenterは、今までMVCでViewControllerが行なってきたことのほとんどを担っており、ControllerとModelの橋渡し的存在です。

このように責務を分け合うことによってFatViewControllerを回避しています。

##実装コード

![ezgif-4-6912d03e5d.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112697/4c33398e-8a66-855d-7cf2-1312b3d9bdf1.gif)


GitHubAPIを叩いて、TableViewに表示するサンプルです。

##Presenter

Presenterの特徴は下記だと考えます。

・UIはノータッチ
・UIKitはインポートしない

```ruby: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に出力するからです。


```ruby:
#// 入力に関するプロトコル
#// 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)
}

```

また、MVCではViewControllerで保持していたModelを
MVPではPresenterで保持します。

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

※注意点ですが、ViewControllerとPresenterがお互いに参照し合うので
weakキーワードを付けて循環参照を防ぎます。

```ruby:
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にとっては、検索バーがタップされたとか画面が遷移したとかはノータッチなのが特徴です。

```ruby:
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の特徴として、

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

だと考えます。


```ruby: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を繋げます。

```ruby:
  #// このinputがpresenterのこと
    private var input: MVPSearchPresenterInput!
   # // ここで外部からPresenterを繋げる
    func inject(input: MVPSearchPresenterInput) {
        self.input = input
    }
```

今回は、キーボードの検索ボタンを押した時にAPI通信を行い
その結果をTableViewに表示したいので
検索ボタンを押した時にPresenterに知らせます。


そしてPresenterからの結果をViewControllerが受け取るため
MVPSearchPresenterOutputプロトコルを準拠して
以下のように結果を受け取れるようにします。

```ruby:
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を繋げます。

つなげるコードは画面遷移に関するRouterクラスで実装しています。

```ruby: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)
    }
}


```


##おわりに

やはり大きなメリットとしてはViewControllerでの処理が減ったのが大きいと感じました。

また、Presenterは他に依存しないため使い回しが効くとも思います。

ソースコードは下記にまとめています。
https://github.com/taro-ken/GitHubSearchMVP