19
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Swift】VIPERとは

Last updated at Posted at 2021-05-03

#はじめに
VIPERを勉強したので、基本的なことをまとめていこうと思います。

#GitHub

#VIPERとは
クリーンアーキテクチャーをiOS向けにしたシステムアーキテクチャのこと。
View, Interactor, Presenter, Entity, Routerの頭文字からとった。
システムアーキテクチャーとは、今までのGUIアーキテクチャー(MVC, MVVM, MVP)のように、Viewとその他みたいな考え方ではなく、画面遷移やAPI通信、データ保存などを考慮した設計のこと。

#それぞれの役割
View: ViewとViewController
Interactor: API通信担当
Presenter: 自分以外の中継役
Entity: データそのもの
Router: 画面遷移担当
ScreenShot 2021-05-02 14.29.03.png

#特徴
・徹底的な疎結合
→Entity以外全てprotocolで繋ぐ

・Presenterは内部で状態をもたない
→いつどのような入力に対しても必ず同じ出力になる。(Entityの違いはある)

・PresenterのメソッドはViewで起きたものに依存した名前にする
→viewDidLoad, buttonDidTappedなど

・ViewとRouter以外はimport UIKitだめ、絶対

・Interactorはデータを返すだけに徹する
→API通信、端末内保存、メソッドで計算しただけなど関係なく、最後はデータを返すだけ。
他のモジュールからはどのようにデータを返したのかわからなくする。
データの返し方はRxSwift, protocol, closureなど、なんでもいい

・Entityに処理を書かない
→純粋にデータを保持した型

#処理の流れ
1.Routerdえ画面を生成し、DI(依存性注入)させる
2.生成された画面を表示
3.ViewからイベントをPresenterに知らせる
→ライフサイクル、ボタンタップ...
4.PresenterはViewから送られてきたイベントの内容に合わせて以下のような処理をする
・Viewに対して画面の更新依頼する
→Viewは依頼された通りに画面を更新する
・Interactorに対してデータの取得依頼をする
→Interactorは依頼されたデータの取得が完了したらPresenterに通知する
・Routerに対して画面遷移の依頼をする
→Routerは依頼された画面へ遷移する

#命名
ScreenShot 2021-05-02 14.34.47.png

#View

・画面の更新
ラベルの文字変更
UITableViewのreload
など

・Presenterへのイベント通知担当
ライフサイクル
ボタンのタップ、セルのタップ
など

GitHubSearchViewController
import UIKit

protocol GitHubSearchView: AnyObject {
    func initView()
    func startLoading()
    func finishLoading()
    func reloadTableView(items: [GitHubSearchEntity])
}

final class GitHubSearchViewController: UIViewController {

    @IBOutlet private weak var textField: UITextField!
    @IBOutlet private weak var searchButton: UIButton!
    @IBOutlet private weak var indicator: UIActivityIndicatorView!
    @IBOutlet private weak var tableView: UITableView!
    
    // presenterへのアクセスはprotocolを介して行う
    private var presenter: GitHubSearchPresentation!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        tableView.delegate = self
        tableView.dataSource = self
        tableView.register(GitHubSearchTableViewCell.nib,
                           forCellReuseIdentifier: GitHubSearchTableViewCell.identifier)
        searchButton.addTarget(self, action: #selector(searchButtonDidTapped), for: .touchUpInside)
        // presenterにイベントを通知
        presenter.viewDidLoad()
        
    }
    
    func inject(presenter: GitHubSearchPresentation) {
        self.presenter = presenter
    }

}

// MARK: - @objc func
@objc private extension GitHubSearchViewController {
    
    func searchButtonDidTapped() {
        // presenterにイベントを通知
        presenter.searchButtonDidTapped(word: textField.text)
    }
    
}

// MARK: - GitHubSearchView
extension GitHubSearchViewController: GitHubSearchView {
    
    func initView() {
        DispatchQueue.main.async {
            self.tableView.isHidden = true
            self.indicator.isHidden = true
        }
    }
    
    func startLoading() {
        DispatchQueue.main.async {
            self.tableView.isHidden = true
            self.indicator.isHidden = false
        }
    }
    
    func finishLoading() {
        DispatchQueue.main.async {
            self.tableView.isHidden = false
            self.indicator.isHidden = true
        }
    }
    
    func reloadTableView(items: [GitHubSearchEntity]) {
        DispatchQueue.main.async {
            self.tableView.reloadData()
        }
    }
    
}

// MARK: - UITableViewDelegate
extension GitHubSearchViewController: UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        // presenterにイベントを通知
        presenter.selectItem(indexPath: indexPath)
    }
    
}

// MARK: - UITableViewDataSource
extension GitHubSearchViewController: UITableViewDataSource {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // presenterにイベントを通知
        return presenter.getSearchedItems().count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: GitHubSearchTableViewCell.identifier,
                                                 for: indexPath) as! GitHubSearchTableViewCell
        // presenterにイベントを通知
        let item = presenter.getSearchedItems()[indexPath.row]
        cell.configure(gitHubSearch: item)
        return cell
    }
    
}

#Interactor
・ビジネスロジック担当(Utility)
・Presenterから依頼されたビジネスロジックを実装し、結果を返す。
delegate, closure, RxSwift...
・import UIKitだめ、絶対
UIを気にしない

GitHubSearchInteractor
import Foundation

protocol GitHubSearchUsecase {
    func get(parameters: GitHubSearchParameters,
             handler: ResultHandler<[GitHubSearchEntity]>?)
    func getSearchedItems() -> [GitHubSearchEntity]
}

// 他のアーキテクチャーでいうUtilityの役割も持つ
final class GitHubSearchInteractor {
    
    private var searchedItems: [GitHubSearchEntity]
    
    init() {
        searchedItems = []
    }
    
}

// MARK: - GitHubSearchUsecase
extension GitHubSearchInteractor: GitHubSearchUsecase {
    
    func get(parameters: GitHubSearchParameters,
             handler: ResultHandler<[GitHubSearch]>? = nil) {
        guard parameters.validation else {
            handler?(.failure(.error))
            return
        }
        guard let url = URL(string: "https://api.github.com/search/repositories?\(parameters.queryParameter)") else {
            handler?(.failure(.invalidUrl))
            return
        }
        let task = URLSession.shared.dataTask(with: url) { data, _, _ in
            guard let data = data,
                  let gitHubResponse = try? JSONDecoder().decode(GitHubSearchEntityResponse.self, from: data),
                  let items = gitHubResponse.items else {
                handler?(.failure(.error))
                return
            }
            self.searchedItems = items
            handler?(.success(items))
        }
        task.resume()
    }
    
    func getSearchedItems() -> [GitHubSearchEntity] {
        return searchedItems
    }
    
}

#Presenter
・Viewから受け取ったイベントを元に別クラスに依頼
Viewに対して画面更新を依頼
Interactorに対してデータの取得を依頼
Routerに対して画面遷移を依頼

・Presenterが提供するメソッド名はViewのメソッド名と同じ
viewDidLoad, buttonDidTapped...

・Presenterに状態を持たせない

・import UIKitだめ、絶対
UIを気にしない

GitHubSearchPresenter
import Foundation

protocol GitHubSearchPresentation: AnyObject {
    func viewDidLoad()
    func searchButtonDidTapped(word: String?)
    func selectItem(indexPath: IndexPath)
    func getSearchedItems() -> [GitHubSearchEntity]
}

// 他との部品以外はパラメータを持たない
// 他との中継役にだけに徹する
final class GitHubSearchPresenter {
    
    // view, interactor, routerへのアクセスはprotocolを介して行う
    // 循環参照しないようにviewだけweak
    private weak var view: GitHubSearchView?
    private var interactor: GitHubSearchUsecase
    private var router: GitHubSearchWireframe
    
    init(view: GitHubSearchView,
         interactor: GitHubSearchUsecase,
         router: GitHubSearchWireframe) {
        self.view = view
        self.interactor = interactor
        self.router = router
    }
    
}

// MARK: - GithubSearchPresentation
extension GitHubSearchPresenter: GitHubSearchPresentation {
    
    func viewDidLoad() {
        view?.initView()
    }
    
    func searchButtonDidTapped(word: String?) {
        let parameters = GitHubSearchParameters(searchWord: word)
        view?.startLoading()
        interactor.get(parameters: parameters) { [weak self] result in
            guard let self = self else { return }
            self.view?.finishLoading()
            switch result {
                case .success(let items):
                    self.view?.reloadTableView(items: items)
                case .failure(let error):
                    self.router.showAlert(error: error)
            }
        }
    }
    
    func selectItem(indexPath: IndexPath) {
        let gitHubSearchEntity = interactor.getSearchedItems()[indexPath.row]
        let initParameters: WebUsecaseInitParameters = .init(entity: gitHubSearchEntity)
        router.showWeb(initParameters: initParameters)
    }
    
    func getSearchedItems() -> [GitHubSearchEntity] {
        return interactor.getSearchedItems()
    }
    
}

#Entity
・データ構造そのもの

・ロジックを持たせない

・import UIKitだめ、絶対
UIを気にしない

//対応がわかりやすいように置き換え
typealias GitHubSearchEntityResponse = GitHubResponse
typealias GitHubSearchEntity = GitHubSearch
typealias GitHubSearchntityError = GitHubError
import Foundation

struct GitHubResponse: Codable {
    let items: [GitHubSearch]?
}

struct GitHubSearch: Codable {
    
    let id: Int
    let name: String
    private let fullName: String
    var urlString: String { "https://github.com/\(fullName)" }
    
    enum CodingKeys: String, CodingKey {
        case id
        case name
        case fullName = "full_name"
    }
    
}

#Router

・画面遷移

・依存性注入(後述)

・VIPERの肝
VIPERでは画面遷移の処理をRouterで行うことにより、Viewの責務を減らせて可読性の向上が望める

GitHubSearchRouter
import UIKit

protocol GitHubSearchWireframe {
    func showWeb(initParameters: WebUsecaseInitParameters)
    func showAlert(error: Error)
}

final class GitHubSearchRouter {
    
    private unowned let viewController: UIViewController
    
    private init(viewController: UIViewController) {
        self.viewController = viewController
    }
    
    // Routerが画面遷移を担当しているので、ここに書く
    static func assembleModules() -> UIViewController {
        let view = UIStoryboard.gitHubSearch.instantiateInitialViewController() as! GitHubSearchViewController
        let interactor = GitHubSearchInteractor()
        let router = GitHubSearchRouter(viewController: view)
        // presenterが中継役なので、全てと繋げる
        let presenter = GitHubSearchPresenter(view: view,
                                              interactor: interactor,
                                              router: router)
        // viewからpresenterに通知する必要があるため繋ぐ
        // viewとpresenterは互いが互いを知っている
        view.inject(presenter: presenter)
        return view
    }
    
}

// MARK: - GitHubSearchWireframe
extension GitHubSearchRouter: GitHubSearchWireframe {
    
    func showWeb(initParameters: WebUsecaseInitParameters) {
        let next = WebRouter.assembleModules(initParameters: initParameters)
        viewController.show(next: next)
    }
    
    func showAlert(error: Error) {
        print(error.localizedDescription)
    }
    
}

#おわりに
その他の処理はGitHubをご覧ください。

19
17
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
19
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?