LoginSignup
0
1

More than 1 year has passed since last update.

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

Posted at

本記事について

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

VIPERアーキテクチャとは?

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

スクリーンショット 2021-08-22 20.18.06.png

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

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

全体像

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

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

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を学びましたが、別のアーキテクチャも学び比較していければと思います。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1