[swift]MVPアーキテクチャでクソ簡単なアプリを作成してみた

#MVPとは
Model-View-Presenterを軸とした設計のパターンのこと。

#なぜMVPを使うのか
MVCモデルだとなんでもかんでも処理をViewControllerに詰め込む、いわゆるFatViewControllerになってしまうのでそれを脱却するためにMVCを使おうというわけです。

#MVPの特徴
おおまかに説明すると…

・ViewControllerはModelに依存しない
・ViewControllerはUtility(APIなど)に依存しない
・Viewの表示に関わること以外全てをPresenterに任せる
・ifなどの制御文はViewControllerには書かない

ということが最大の特徴かと思います。

#完成形

TableViewのCellをタップするとLabelが選択した文字に変わります。

![スクリーンショット-2021-09-26-16.43.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1112697/a753e1e2-0ceb-9ac9-1870-e63794f518cb.png)


#コード

###①View

```ruby:ViewController.swift

import UIKit

class ViewController: UIViewController {
   # //storyboardからVCを生成する
    static func makeFromStoryboard() -> ViewController {
            let vc = UIStoryboard.ViewController
            return vc
        }
    
    private var outputText: String = "なにも選択されていません"
    
    
    @IBOutlet weak var outputLabel: UILabel!{
        didSet{
            outputLabel.text = outputText
        }
        
    }
    
   # //ここでPresenterとViewControllerを繋げる
    private var presenter: PresenterInput!
    func inject(presenter:PresenterInput){
        self.presenter = presenter
    }
    
    
    
    @IBOutlet weak var tableView: UITableView!{
        didSet{
            tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
            tableView.delegate = self
            tableView.dataSource = self
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
    }

    
}
#//Outputプロトコルに準拠し、Presenter出力処理を受け取る
extension ViewController:PresenterOutput{
    func update(text: String) {
        outputLabel.text = text
    }
    
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
       # //PresenterInputで受け取ったModelをここで使う
        presenter.numberOfItems
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
       # //PresenterInputで受け取ったModelをindex番目Cellにいれる
        let item = presenter.item(index: indexPath.row)
        cell.textLabel?.text = item.title
        return cell
    }
}

extension ViewController: UITableViewDelegate {
   # // cellを選択したときの処理
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        presenter.didSelect(index: indexPath.row)
    }
}


```

###②Model
```ruby:Model.swift

import Foundation

struct SampleItem {
    let title: String

    static func createItems() -> [SampleItem] {
        let items = [
            SampleItem(title: "りんご"),
            SampleItem(title: "バナナ"),
            SampleItem(title: "オレンジ"),
            SampleItem(title: "ぶどう"),
            SampleItem(title: "キウイ")
        ]
        return items
    }
}


```

###③Presenter
```ruby:Presenter.swift

import Foundation

#//ViewController側からPresenterに「情報くれ」と通知することになるので、Inputの処理
protocol PresenterInput {
    var numberOfItems: Int {get}
    func item(index:Int)->SampleItem
    func didSelect(index: Int)
   
    
}

protocol PresenterOutput :AnyObject{
    func update(text: String)
    
}


final class Presenter {
    private var output:PresenterOutput?
   # //データをここで持つ
    private var items:[SampleItem] = SampleItem.createItems()
    
    init(output:PresenterOutput) {
        self.output = output
    }
    
}
#//Inputプロトコルに準拠して、入力処理を行う
#//vcからのInputを受け付ける
extension Presenter:PresenterInput{

# //itemsとしてModelデータを保持しているので、外部からModelを受け取れる。
    var numberOfItems: Int {
        items.count
    }
    
    func item(index: Int) -> SampleItem {
        items[index]
    }
    
    func didSelect(index: Int) {
        output?.update(text: items[index].title)
    }
    
}
```
###④AppDelegate、Router（画面をコードで立ち上げる）
```ruby:AppDelegate.swift

import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let window = UIWindow(frame: UIScreen.main.bounds)
        self.window = window
        Router.showRoot(window: window)
        return true
    }

}

```


```ruby:Router.swift

import UIKit

final class Router {
    static func showRoot(window: UIWindow?) {
        let vc = ViewController.makeFromStoryboard()
        
      # //ViewControllerで用意したinjectメソッドをここで呼び出す。
        let presenter = Presenter(output: vc)
        vc.inject(presenter: presenter)
        let nav = UINavigationController(rootViewController: vc)
        window?.rootViewController = nav
        window?.makeKeyAndVisible()
    }
 

}
```

###⑤Storyboard+（Storyboardを生成）
```ruby:Storyboard+.swift
import UIKit

#// Storyboardの読み込みを１箇所にまとめる
extension UIStoryboard {
  static var ViewController: ViewController {
    UIStoryboard.init(name: "Main", bundle: nil).instantiateInitialViewController() as! ViewController
  }

  static var secondViewController: SecondViewController {
    UIStoryboard.init(name: "Second", bundle: nil).instantiateInitialViewController() as! SecondViewController
  }

}

```

#所感
MVCに比べ、あきらかにViewCintrollerがスッキリしました。

開発の規模が大きくなるほど、ViewControllerがFatになりやすいと思うのでこのような構成が非常に大事と感じました。