MVCからVIPERに変えて得られたこと

## 本記事について

本記事では、VIPERアーキテクチャについて学習し、MVCからVIPERに変えて得られたことについてご紹介します。

## VIPERアーキテクチャとは？

- VIPERとは、単一責務原則に基づき、Cleanでモジュール化された構造を実現するアーキテクチャ
- 「VIPER」は、`View`、`Interactor`、`Presenter`、`Entity`、`Router` の頭文字を取った造語

![スクリーンショット 2021-08-22 20.18.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/477704/1900631d-3dd0-c1d5-5a84-2204354723be.png)

## 雑なMVCからVIPERアーキテクチャに変えてみた

過去にMVC開発したアプリをVIPERアーキテクチャに変えて、得られる恩恵について紹介します。

### 全体像

今回はリスト表示の箇所をVIPERに変えます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/477704/97ee61f6-1c2b-3403-fbdf-a77780412722.png" width="700">

クラス図は以下の通りです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/477704/6d20ef09-0ca8-0f9a-e108-49efbafe60e5.png" width="700">

### View

- 役割
    - Presenterから指示された画面の更新
    - Presenterへのイベントの通知

```CharaListViewController.swift

import UIKit

protocol CharaListViewInterface: AnyObject {
    func displayCharaList(_ chara: [CharaEntity])
    func displayLodingAlert()
    func displayFinishLodingAlert()
    func alertListError(error: Error)
}


final class CharaListViewController: UIViewController {
    
    @IBOutlet weak var charaCollectionView: UICollectionView! {
        didSet {
            charaCollectionView.dataSource = self
            charaCollectionView.delegate = self
            charaCollectionView.registerNib(cellType: CharaCollectionViewCell.self)
            charaCollectionView.backgroundColor = Asset.viewBgColor.color
        }
    }
    
    @IBOutlet weak var postCharaButton: UIButton! {
        didSet {
            postCharaButton.allMaskCorner()
            postCharaButton.addTarget(self, action: #selector(movePost), for: .touchUpInside)
        }
    }
    
    // プロトコルを介してPresenterへアクセス
    var presenter: CharaListPresenterInterface!
    private var chara: [CharaEntity] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        presenter.viewDidLoad()
    }
}
    
// MARK: - private method

private extension CharaListViewController {
    @objc
    private func movePost() {
        presenter.didTapCharaPost()
    }
}

// MARK: - Interface

extension CharaListViewController: CharaListViewInterface {
    
    func displayCharaList(_ chara: [CharaEntity]) {
        self.chara = chara
        self.searchResultChara = chara
        DispatchQueue.main.async {
            self.charaCollectionView.reloadData()
        }
    }
    
    func displayLodingAlert() {
        self.startLoad()
    }
    
    func displayFinishLodingAlert() {
        self.doneMessage()
    }
    
    func alertListError(error: Error) {
        self.alertError(error: error)
    }
    
}

// MARK: - UICollection Delegate

extension CharaListViewController: UICollectionViewDataSource {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.searchResultChara.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(for: indexPath) as CharaCollectionViewCell
        let component = CharaCollectionViewCell.Component(charaInfo: self.searchResultChara[indexPath.row])
        cell.setupCell(component: component)
        return cell
    }
    
}

extension CharaListViewController: UICollectionViewDelegate {
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        self.presenter.didSelectChara(selectedChara: self.searchResultChara[indexPath.row])
    }
    
}

```

### Presenter

- 役割
    - 各要素間の橋渡し役

```CharaListPresenter.swift

import Foundation

protocol CharaListPresenterInterface: AnyObject {
    func viewDidLoad()
    func didSelectChara(selectedChara: CharaEntity)
    func didTapCharaPost()
}

final class CharaListPresenter {
    private let view: CharaListViewInterface
    private let interactor: CharaListInteractorInterface
    private let router: CharaListRouterInterface
    
    init(
        view: CharaListViewInterface,
        interactor: CharaListInteractorInterface,
        router: CharaListRouter
    ) {
        self.view = view
        self.interactor = interactor
        self.router = router
    }
}

extension CharaListPresenter: CharaListPresenterInterface {
    func viewDidLoad() {
        self.view.displayLodingAlert()
        
        self.interactor.fetchCharaList { result in
            self.view.displayFinishLodingAlert()
            
            switch result {
            case let .success(characters):
                self.view.displayCharaList(characters)
            case let .failure(error):
                self.view.alertListError(error: error)
            }
        }
    }
    
    func didSelectChara(selectedChara: CharaEntity) {
        interactor.saveSpotlightChara(selectedChara: selectedChara)
        interactor.saveUserDefaultsChara(selectedChara: selectedChara)
        router.pushCharaDetail(chara: selectedChara)
    }
    
    func didTapCharaPost() {
        router.presentCharaPost()
    }
}

```

### Interactor

- 役割
    - データ操作とユースケースの定義

```CharaListInteractor.swift

import Foundation

protocol CharaListInteractorInterface: AnyObject {
    func fetchCharaList(_ completion: @escaping ((Result<[CharaEntity], Error>) -> Void))
    func saveSpotlightChara(selectedChara: CharaEntity)
    func saveUserDefaultsChara(selectedChara: CharaEntity)
}

final class CharaListInteractor: CharaListInteractorInterface {
    private let firebaseRepository: FirebaseRepositoryProtocol
    private let spotlight: SpotlightRepositoryProtocol
    
    init(firebaseRepository: FirebaseRepositoryProtocol, spotlight: SpotlightRepositoryProtocol) {
        self.firebaseRepository = firebaseRepository
        self.spotlight = spotlight
    }
    
    func fetchCharaList(_ completion: @escaping ((Result<[CharaEntity], Error>) -> Void)) {
        firebaseRepository.fetchChara { completion($0) }
    }
    
    func saveSpotlightChara(selectedChara: CharaEntity) {
        spotlight.saveChara(charaInfo: selectedChara)
    }
    
    func saveUserDefaultsChara(selectedChara: CharaEntity) {
        UserDefaultsClient().saveChara(
            selectedChara,
            forkey: "chara://detail?id=\(selectedChara.id)"
        )
    }
}

```

### Entity

- 役割
    - 各要素間でやり取りされるデータ

```CharaEntity.swift

import Foundation

struct CharaEntity: Codable {
    var id: Int
    var name: String
    var description: String
    var imageRef: String
    var isFavorite: Bool = false
    var order: Date
}

```

### Router

- 役割
    - 画面遷移とDI
    - Presenterから画面遷移の依頼を受け取る

```CharaListRouter.swift

import UIKit

protocol CharaListRouterInterface: AnyObject {
    func pushCharaDetail(chara: CharaEntity)
    func presentCharaPost()
}

final class CharaListRouter {
    private let viewController: CharaListViewController
    
    init(viewController: CharaListViewController) {
        self.viewController = viewController
    }
    
    static func assembleModule() -> CharaListViewController {
        let storyboard = UIStoryboard(name: "CharaList", bundle: nil)
        let viewController = storyboard.instantiateViewController(withIdentifier: "list") as! CharaListViewController
        let interactor = CharaListInteractor(firebaseRepository: FirebaseRepository(), spotlight: SpotlightRepository())
        let router = CharaListRouter(viewController: viewController)
        
        let presenter = CharaListPresenter(
            view: viewController,
            interactor: interactor,
            router: router
        )
        
        viewController.presenter = presenter
        return viewController
    }
}

extension CharaListRouter: CharaListRouterInterface {
    func pushCharaDetail(chara: CharaEntity) {
        let storyboard = UIStoryboard(name: "CharaDetail", bundle: nil)
        let nextView = storyboard.instantiateViewController(withIdentifier: "detail") as! CharaDetailViewController
        viewController.navigationController?.pushViewController(nextView, animated: true)
    }
    
    func presentCharaPost() {
        let storyboard = UIStoryboard(name: "CharaPost", bundle: nil)
        let nextView = storyboard.instantiateViewController(withIdentifier: "post") as! CharaPostViewController
        viewController.present(nextView, animated: true)
    }
}


```

## 得られたメリット
### 単一責務
MVCやMVVMとは違い、必然と凝集度の高いメソッドの定義ができると感じました。

例えば、タップ時の処理について
MVCだと色々処理が混ざってしまいます。当然メソッドの切り分けはできますが、規模が大きくなると、ViewControllerが肥大化するため、見通しは悪くなると思います。

```CharaListViewController（MVC）.swift
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        spotlight.saveChara(charaInfo: model.entity[indexPath.row])
        UserDefaultsClient().saveChara(
            model.entity[indexPath.row],
            forkey: "chara://detail?id=\(model.entity[indexPath.row].id)"
        )
        
        let storyboard: UIStoryboard = UIStoryboard(name: "CharaDetail", bundle: nil)
        let nextView = storyboard.instantiateViewController(withIdentifier: "detail") as! CharaDetailViewController
        
        nextView.charaDetail = model.entity[indexPath.row]
        if let tmp = UserDefaultsClient().loadChara(key: "favo=\(model.entity[indexPath.row].id)") {
            nextView.charaDetail.isFavorite = tmp.isFavorite
        }
        self.navigationController?.pushViewController(nextView, animated: true)
    }

``` 

しかし、VIPERに切り替えることで、かなり見通しが良くなり、「どの処理が何をしているのか」が明確になります。

```CharaListPresenter.swift
    func didSelectChara(selectedChara: CharaEntity) {
        interactor.saveSpotlightChara(selectedChara: selectedChara)
        interactor.saveUserDefaultsChara(selectedChara: selectedChara)
        router.pushCharaDetail(chara: selectedChara)
    }
```

## 最後に

今回はVIPERを学びましたが、別のアーキテクチャも学び比較していければと思います。