MVPでGitHubのAPIを叩いてTableViewに結果を表示する

## はじめに
MVPアーキテクチャでGitHubのAPIを叩いて結果を表示するアプリを作りました。
MVPはレガシーになってきており、アーキテクチャの主流はMVVMらしいですが、そこを学ぶ前段階としてまずはMVPについて整理しました。

参考にさせて頂いたのは名著「iOSアプリ設計パターン入門」です。

https://gyazo.com/eb5c8c1c99ed56121d0ee87da88ff023

## MVPの大まかな構図

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/943089/6337ffd8-6dfb-40be-05e9-7cb7f793465c.png)

①ViewからUI操作を受けて、Presenterへ通知。
②PresenterがModelを操作。
③PresenterがViewに受け取ったModelの結果を渡す。

これをイメージとして持っておくと理解がしやすいです。

## View
まずはViewから。
Viewは描画処理のみを担当します。いつViewが変更されるかなどは知りません。また、PresenterにUI操作の通知を行います。

カスタムCellは省きます。

```swift:MVPSearchController.swift
import UIKit

final class MVPSearchController: UIViewController {
    
    @IBOutlet private weak var indicator: UIActivityIndicatorView!
    @IBOutlet private weak var tableView: UITableView! {
        didSet {
            tableView.register(UINib(nibName: "RepoCell", bundle: nil), forCellReuseIdentifier: "RepoCell")
            tableView.dataSource = self
            tableView.delegate = self
        }
    }
    @IBOutlet private weak var searchText: UITextField!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.isHidden = true
        indicator.isHidden = true
    }
    
    // Presenterのインスタンスを保持
    private var presenter: GitHubPresenterInput!
    // Routerに繋いでもらう処理がある
    func inject(presenter: GitHubPresenterInput) {
        // Presenterと繋がる
        self.presenter = presenter
    }
   
    // 検索ボタンタップ
    @IBAction func search(_ sender: Any) {
        guard let searchText = searchText.text else { return }
        // Presenterに通知
        presenter.search(with: searchText)
    }
}

extension MVPSearchController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // Presenterに通知
        presenter.selected(index: indexPath.row)
    }
}

extension MVPSearchController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // Presenterに通知
        return presenter.numberOfItems
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "RepoCell") as? RepoCell else
        { return UITableViewCell() }
        
        let repositoly = presenter.getRepo(index: indexPath.row)
        cell.configure(repositoly: repositoly)
        
        return cell
    }
}

extension MVPSearchController: GitHubPresenterOutput {
    func upDataRepsitory(_ repository: [Repository]) {
        DispatchQueue.main.async {
            self.tableView.reloadData()
        }
    }
    
    func upData(load: Bool) {
        DispatchQueue.main.async {
            self.tableView.isHidden = load
            self.indicator.isHidden = !load
        }
    }
    
    func get(error: Error) {
        print(error.localizedDescription)
    }
    
    func showWeb(Repositoly: Repository) {
        DispatchQueue.main.async {
            Router.shared.showWeb(from: self, repositoly: Repositoly)
        }
    }
}

```

UI操作をPresenterに通知して結果を受け取っています。先ほどの構図通りです。

また、ここでのポイントは具体的な描画処理をProtocolとして切り分けている所です。

GitHubPresenterOutputの処理が呼ばれるタイミングはロジックを担当するPresenterの役割なので、Viewが知る必要はないため、中身だけ実装しておきます。

## Presenter
続いて、Presenterです。
Presenterはロジック部分を担当します。Viewからの通知を受けて、Modelの情報を返すのが主な役割です。

```swift:GitHubPresenter.swift
import Foundation

// アクションを受け入れる用のプロトコル
protocol GitHubPresenterInput {
    var numberOfItems: Int { get }
    func getRepo(index: Int) -> Repository
    func selected(index: Int)
    func search(with text: String?)
}

// アクションを加える用のプロトコル
protocol GitHubPresenterOutput: AnyObject {
    func upDataRepsitory(_ repository: [Repository])
    func upData(load: Bool)
    func get(error: Error)
    func showWeb(Repositoly: Repository)
}

class GitHubPresenter {
    // アクションを加えるプロトコルに準拠したインスタンスを保持(Viewを保持)
    private weak var output: GitHubPresenterOutput!
    // API通信用に準拠したインスタンスを取得
    private var api: GitHubAPIProtocol
    // モデルを保持
    private var repositoly: [Repository]
    
    init(output: GitHubPresenterOutput, api: GitHubAPIProtocol = GitHubAPI.shared, repositoly: [Repository] = []) {
        self.output = output
        self.api = api
        self.repositoly = repositoly
    }
}

// PresenterはInputを受けてOutputするイメージ(外から通知を受けて外に通知を出す)
// outputが具体的に何をしてるかは知らなくても良い
extension GitHubPresenter: GitHubPresenterInput {
    // 数を数えて！という通知に対して
    var numberOfItems: Int { return repositoly.count }
    // 指定されたindexのレポジトリを教えて！という通知に対して
    func getRepo(index: Int) -> Repository {
        return repositoly[index]
    }
    // 指定されたindexのレポジトリが選択された！という通知に対して
    // webを開く通知をする
    func selected(index: Int) {
        self.output.showWeb(Repositoly: repositoly[index])
    }
    // 指定されたtextをもとに検索して！
    func search(with text: String?) {
        guard let text = text, !text.isEmpty else { return }
        // ロード中を通知
        self.output.upData(load: true)
        self.api.getRepository(searchWord: text) { [weak self] result in
            // ロード終了を通知
            self?.output.upData(load: false)
            // モデルを操作してViewに結果を渡す
            switch result {
            case .success(let repositoly):
                self?.repositoly = repositoly
                self?.output.upDataRepsitory(repositoly)
            case .failure(let error):
                self?.output.get(error: error)
            }
        }
    }
}
```
さきほどの構図通り、PresenterはViewを変数outputとして保持し、Modelを変数repositolyとして保持していますね。ViewがPresenterに対して通知するときに呼び出されるGitHubPresenterInputプロトコルのメソッドに応じて、Viewに対して描画指示を出しています。
output(View)が行う具体的な処理内容の記述はViewのファイルに記述してます。

## Model
続いてはModelです。

```swift:Repository.swift
import Foundation

struct Item: Codable {
    let items: [Repository]?
}

struct Repository: Codable {
    let fullName: String
    var urlStr: String {
        return "https://github.com/\(fullName)"
    }
    
    enum CodingKeys: String, CodingKey {
        case fullName = "full_name"
    }
}
```

Modelにはロジックは持たせていません。そのかわり、API通信用のプロトコルに準拠させた新たなクラスを作り、そこに通信用の処理を記述します。Modelは値の受け渡し専用みたいな感じです。
そうすることで責務の切り分けができます。

## 通信用のクラス
続いてはAPI通信用のシングルトンのクラスです。

```swift:GitHubAPI.swift
import Foundation

enum GitHubError: Error {
    case error
}
// 実際に検索処理をするプロトコル
protocol GitHubAPIProtocol {
    func getRepository(searchWord: String, completion: ((Result<[Repository], GitHubError>) -> Void)?)
}

final  class GitHubAPI: GitHubAPIProtocol {
    static let shared = GitHubAPI()
    private init() {}
    
    func getRepository(searchWord: String, completion: ((Result<[Repository], GitHubError>) -> Void)?) {
        guard searchWord.count >= 0 else {
            completion?(.failure(.error))
            return
        }
        
        guard let url = URL(string: "https://api.github.com/search/repositories?q=\(searchWord)&sort=stars") else { return }
        
        // API通信
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            guard let data = data else { return }
            guard let repositolies = try? JSONDecoder().decode(Item.self, from: data) else { return }
            guard let repositoly = repositolies.items else { return }
            if error != nil {
                completion?(.failure(.error))
                return
            }
            completion?(.success(repositoly))
        }
        // 検索処理実行
        task.resume()
    }
}
```

GitHubAPIProtocolに準拠させて、デリゲートメソッドの中身はGitHubAPI内に記述しています。「iOSアプリ設計パターン入門」ではModel内にこの処理を記述しています。その方が先ほどの構図通りですが、責務を切り分けるために、シングルトンクラスを用意しました。

Presenterでデータを取得したいときは、GitHubAPIクラスのgetRepository()を呼んで、取得します。

## Router
続いては、画面遷移の処理をまとめるRouterクラスです。

```swift:Router.swift
final class Router {
    static let shared = Router()
    private init() {}
    private var window: UIWindow?
    
    // MVPSearchViewControllerを表示
    func showRoot(window: UIWindow) {
        guard let vc = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as? MVPSearchController else { return }
        // vcとpresenterを参照させ合う(presenterは弱参照でvcを保持)
        let presenter = GitHubPresenter(output: vc)
        vc.inject(presenter: presenter)
        
        let nav = UINavigationController(rootViewController: vc)
        window.rootViewController = nav
        window.makeKeyAndVisible()
        self.window = window
    }
    
    // 次の画面へ遷移
    func show(from: UIViewController, to: UIViewController) {
        if let nav = from.navigationController {
            nav.pushViewController(to, animated: true)
        } else {
            from.present(to, animated: true, completion: nil)
        }
    }
    
    // show()を使ってWebViewControllerに画面遷移
    func showWeb(from: UIViewController, repositoly: Repository) {
        guard let webVc = UIStoryboard(name: "Web", bundle: nil).instantiateInitialViewController() as? WebViewController else { return }
        let presenter = WebPresenter(output: webVc, repositoly: repositoly)
        webVc.inject(presenter: presenter)
        show(from: from, to: webVc)
    }
}
```

AppDelegateでshowRoot()を呼んで、MVPSearchViewControllerを表示します。

showRoot()では、MVPSearchViewControllerのインスタンスを生成し、inject()を呼んで,
ViewであるMVPSearchViewControllerとPresenterを繋ぎます。そして、ナビゲーションコントローラーから遷移してMVPSearchViewControllerが表示されるという処理が記述されてます。

showWeb()では、WebViewControllerのインスタンスを生成し、inject()を呼んで、ViewであるWebViewControllerとWebPresenterを繋ぎます。そして、画面遷移するメソッドであるshow()を使ってWebViewControllerに遷移するという処理が記述されてます。

## View(2)
続いては、WebViewControllerですね。ここではレポジトリのURLを叩いてWebサイトを表示します。

```swift:WebViewController.swift
import UIKit
import WebKit

final class WebViewController: UIViewController {
    
    @IBOutlet private weak var webView: UIWebView!
    // presenterのインスタンスを保持
    private var presenter: WebPresenter!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // viewDidLoaded()の中身をVCが知る必要はない
        presenter.viewDidLoaded()
    }
    
    func inject(presenter: WebPresenter) {
        self.presenter = presenter
    }
}

extension WebViewController: WebPresenterOutput {
    func load(request: URLRequest) {
        DispatchQueue.main.async {
            self.webView.loadRequest(request)
        }
    }
}
```

PresenterにviewDidLoaded()で通知して、処理を仰ぎます。そうすると、Presenterはload()を呼んで、Webサイトを表示します。

## Presenter(2)
続いては、WebPresenterクラスです。

```swift:WebPresenter.swift
import Foundation

// アクションを受け入れるプロトコル
protocol WebPresenterInput {
    func viewDidLoaded()
}

// アクションを加えるプロトコル
protocol WebPresenterOutput: AnyObject {
    func load(request: URLRequest)
}

final class WebPresenter {
    private weak var output: WebPresenterOutput!
    private var repositoly: Repository
    
    init(output: WebPresenterOutput, repositoly: Repository) {
        self.output = output
        self.repositoly = repositoly
    }
}


extension WebPresenter: WebPresenterInput {
    func viewDidLoaded() {
        guard let url = URL(string: repositoly.urlStr) else { return }
        output.load(request: URLRequest(url: url))
    }
}
```
viewDidloaded()の中身はここで記述してあげます。ViewはviewDidloaded()を使って通知するだけなので中身を知る必要がありませんでした。


以上が、MVPの基本的な扱い方になります。

## 参考

『iOSアプリ設計パターン 入門』 第6章 MVP
関義隆、史翔新、田中賢治、松館大輝、鈴木大貴、杉上洋平、加藤寛人