目次
1.はじめに
2.VIPERとは
3.VIPERのそれぞれの役割
4.View
5.Interactor
6.Presenter
7.Entity
8.Router
9.最後に
参考
1.はじめに
iosアプリ開発を勉強していくうちに必ずぶち当たるアーキテクチャの一つであるVIPERについて記事を書きたいと思います。
今回は地図アプリを作成します。
僕はVIPERのサンプルをコーディングをしていくうちに感動しました。
2.VIPERとは
VIPERとはView・Interactor・Presenter・Entity・Routerのそれぞれの頭文字をとったものです。
また基本としてDI(Dependency Injection)で外部から注入し、単一原則のもとに分割します。
MVC、MVVMなどのアーキテクチャはViewが画面遷移も担当する必要があります。
3.VIPERのそれぞれの役割
| 役割 | |
|---|---|
| View | UIの更新やPresenterにViewイベントの通知 |
| Interactor | ビジネスロジックを担当 |
| Presenter | ハブの役割※ |
| Entity | データ構造の定義(Model) |
| Router | 画面遷移 |
※ViewにUISearchBarがあったとしてエンターを押したときにPresenterへ通知し、PresenterはInteractorにデータの取得を依頼するなど
4.View
早速Viewのコードを見ていきます。
import UIKit
import MapKit
①
protocol MapView: AnyObject {
func updatePin(coordinate: CLLocationCoordinate2D)
func showErrorAlert()
}
final class MapViewController: UIViewController, Storyboardable {
②
var presenter: MapPresentation!
private let pin = MKPointAnnotation()
@IBOutlet private weak var mapView: MKMapView!
private var searchBar: UISearchBar = {
let searchBar = UISearchBar()
searchBar.placeholder = "住所"
searchBar.autocorrectionType = .no
searchBar.autocapitalizationType = .none
return searchBar
}()
override func viewDidLoad() {
navigationItem.titleView = searchBar
searchBar.delegate = self
}
}
extension MapViewController: MapView {
func updatePin(coordinate: CLLocationCoordinate2D) {
guard let adress = searchBar.text else { return }
updateScreenCenter(coordinate: coordinate)
pin.title = adress
pin.coordinate = coordinate
mapView.addAnnotation(pin)
}
func showErrorAlert() {
let alertController = UIAlertController(title: "No Result", message: "該当する検索結果がありませんでした", preferredStyle: .alert)
let ok = UIAlertAction(title: "OK", style: .default)
alertController.addAction(ok)
present(alertController, animated: true)
}
}
extension MapViewController: UISearchBarDelegate {
③
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
guard let searchText = searchBar.text else { return }
presenter.searchButtonPushed(adress: searchText)
}
}
private extension MapViewController {
func updateScreenCenter(coordinate: CLLocationCoordinate2D) {
let span = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
let region = MKCoordinateRegion(center: coordinate, span: span)
mapView.region = region
}
}
①はPresenterから呼び出す関数をprotocolで定義?します。
| 関数名 | 機能 |
|---|---|
| updatePin(coordinate: ) | 検索した住所の位置にピンをさす |
| func showErrorAlert() | エラーが発生したらアラートを出す |
②はpresenterにイベントを通知するために定義します。
③で検索したい住所をpresenterに渡しています。
5.Interactor
import CoreLocation
protocol MapUsecase: AnyObject {
func searchAdress(adress: String, completion: @escaping (Result<CLLocationCoordinate2D, Error>) -> Void)
}
final class MapInteractor {
}
extension MapInteractor: MapUsecase {
func searchAdress(adress: String, completion: @escaping (Result<CLLocationCoordinate2D, Error>) -> Void) {
Geocoder.fetchAdress(adress: adress) { result in
switch result {
case .success(let coordinate):
completion(.success(coordinate))
break
case .failure(let error):
completion(.failure(error))
break
}
}
}
}
Interactorはビジネスロジックを担当します。
特に説明はいらないと思いますが、APIを叩くなどです。
6.Presenter
import Foundation
protocol MapPresentation: AnyObject {
func searchButtonPushed(adress: String)
}
final class MapPresenter {
private weak var view: MapView?
private let router: MapWriteframe
private let interactor: MapUsecase
①
init(view: MapView, router: MapWriteframe, interactor: MapUsecase) {
self.view = view
self.router = router
self.interactor = interactor
}
}
extension MapPresenter: MapPresentation {
②
func searchButtonPushed(adress: String) {
//interactorに住所の検索を依頼
interactor.searchAdress(adress: adress) {[weak self] result in
switch result {
case .success(let coordinate):
self?.view?.updatePin(coordinate: coordinate)
break
case .failure:
self?.view?.showErrorAlert()
break
}
}
}
}
①DIの部分です。Presenterはハブの役割なのでそれぞれのインスタンスが必要になります。
②viewでsearchBarSearchButtonClicked(_ searchBar: )が呼び出されたときにsearchBar.textを渡されてinteractorに検索を依頼します。
7.Entity
今回のアプリは自分で定義するEntityはありませんが(CLLocationCoordinate2D)、githubのAPIなどを叩くときにRepositoryEntityなどを定義しましょう。
8.Router
protocol MapWriteframe: AnyObject {
}
final class MapRouter {
①
private weak var viewControlelr: UIViewController?
private init(viewController: UIViewController) {
self.viewControlelr = viewController
}
②
static func assembleModules() -> UIViewController {
let view = MapViewController.instantiate()
let router = MapRouter(viewController: view)
let interactor = MapInteractor()
let presenter = MapPresenter(view: view, router: router, interactor: interactor)
view.presenter = presenter
return view
}
}
extension MapRouter: MapWriteframe {
}
①viewController使ってないのになんで宣言してるのと思われるかもしれないですが、画面遷移する場合に必要になります。(今回は必要ない)
②viewControllerの表示用の関数。
9.最後に
一つの画面ごとにView、Interactor、Presenter、Entity、Routerが必要でめんどくささもありますが、テストのしやすさ、可読性、修正しやすさなど多くのメリットがあります。
僕が一番感動したのはコードの綺麗さです。
自分ができる人みたいな感覚になれておすすめです。
今回なんとなく記事にしてみましたがまだまだ理解が足りていないと感じたので精進します。
アドバイスなどありましたらよろしくお願い致します。