本記事について
本記事では、VIPERアーキテクチャについて学習し、MVCからVIPERに変えて得られたことについてご紹介します。
VIPERアーキテクチャとは?
- VIPERとは、単一責務原則に基づき、Cleanでモジュール化された構造を実現するアーキテクチャ
- 「VIPER」は、
View
、Interactor
、Presenter
、Entity
、Router
の頭文字を取った造語
雑な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を学びましたが、別のアーキテクチャも学び比較していければと思います。