20210506のiOSに関する記事は2件です。

MVPアーキテクチャについて

はじめに 今までは、ずっとMVCを採用してアプリを開発してきましたが MVC脱却を図るため、今回はMVPに関することを備忘録として残しておきます。 まず、MVPを知る前にMVCの流れを復習していきましょう。 MVC API通信の例で簡単に説明すると、 まず、Viewからユーザーのアクションを検知し、そのアクションをControllerに伝えます。 そして、Controllerは必要なModelを取得するためにAPIを叩きます。 その後、APIから必要なModelが返され、Controllerで保持されます。 ControllerからViewにModelを渡して そのModelからViewを更新することによって画面が更新されます。 なぜMVCから脱却したいのか? MVCだとFatViewControllerになりやすいからです。 iOSDC 2017 前夜祭で「節子、それViewControllerやない…、FatViewControllerや…。」というタイトルで登壇しました! 余談ですが、上記の記事もMVPについて書かれてました! MVP 先ほどと同じようにAPI通信の例で説明すると、 まず、Viewからユーザーのアクションを検知します。 そして、Controllerにアクションを伝えてPresenterに知らせます。 Presenterは必要なModelを取得するためにAPIを叩きます。 その後、APIから必要なModelが返され、Presenterで保持されます。 PresenterからControllerにModelを渡して そのModelをViewに渡します。 最終的に、ModelからViewを更新することによって画面が更新されます。 Presenterについて Presenterは、今までMVCでViewControllerが行なってきたことのほとんどを担っています。 APIを叩いたり、Modelを保持したり等ですね。 そのような責務を引き剥がすことによってFatViewControllerを回避しています。 では実際に、コードで確認していきましょう。 実装コード 今回は、Github APIを使用したアプリを例に説明していきます。 Presenter まずは、Presenterのコードです。 Presenterの特徴として、 ・UIがどうなっているかは考慮しない ・UIKitをインポートしない ・Xcodeじゃなくてもコードが書ける などが挙げられます。 iOS特有のUIKit、SwiftUIなどに依存しないので極端にいうとメモアプリでも書けます。 MVPSearchPresenter.swift import Foundation protocol MVPSearchPresenterInput { var numberOfItems: Int { get } func item(index: Int) -> GithubModel func search(param: String?) func didSelect(index: Int) } protocol MVPSearchPresenterOutput: AnyObject { func update(loading: Bool) func update(githubModels: [GithubModel]) func validation(error: ParameterValidationError) func get(error: Error) func showWeb(url: URL) } final class MVPSearchPresenter { private weak var output: MVPSearchPresenterOutput! private var api: GithubAPIProtocol! private var githubModels: [GithubModel] init(output: MVPSearchPresenterOutput, api: GithubAPIProtocol = GithubAPI.shared) { self.output = output self.api = api self.githubModels = [] } } extension MVPSearchPresenter: MVPSearchPresenterInput { var numberOfItems: Int { githubModels.count } func item(index: Int) -> GithubModel { githubModels[index] } func search(param: String?) { if let validationError = ParameterValidationError(param: param) { output.validation(error: validationError) return } guard let searchText = param else { return } output.update(loading: true) self.api.get(searchText: searchText) {[weak self] (result) in guard let self = self else{ return } switch result { case .success(let githubModels): self.output.update(loading: false) if githubModels.isEmpty { self.output.get(error: AppError.emptyApiResponce.error) return } self.githubModels = githubModels self.output.update(githubModels: githubModels) case .failure(let error): self.output.update(loading: false) self.output.get(error: error) } } } func didSelect(index: Int) { guard let githubUrl = URL(string: githubModels[index].urlStr) else { output.get(error: AppError.getApiData.error) return } output.showWeb(url: githubUrl) } } ではコードを細かく説明していきます。 ・コード説明① まず、ViewControllerとPresenterを繋げないといけません。 PresenterがViewControllerからの入力を受け取り その結果をViewControllerに出力しないといけないからです。 MVPでは依存性を低くするためにprotocolによって繋げます。 以下が実際にprotocolを定義している部分です↓ // 入力に関するプロトコル // ViewControllerから送られてくる protocol MVPSearchPresenterInput { var numberOfItems: Int { get } func item(index: Int) -> GithubModel func search(param: String?) func didSelect(index: Int) } // 出力に関するプロトコル // ViewControllerに結果を渡す protocol MVPSearchPresenterOutput: AnyObject { func update(loading: Bool) func update(githubModels: [GithubModel]) func validation(error: ParameterValidationError) func get(error: Error) func showWeb(url: URL) } ・コード説明② Presenterは、Modelを内部で保持します。 更にoutputプロパティを定義し、init時にViewControllerと繋げれるようにしています。 ここで注意すべき点は、ViewControllerとPresenterがお互いに参照し合うので weakキーワードを付けて循環参照にならないようにしないといけません。 クロージャの中に書く[weak self]についてまとめてみた final class MVPSearchPresenter { // ViewControllerとPresenterが参照し合い循環参照が起きるためweakキーワードを付ける // このoutputがViewControllerのこと private weak var output: MVPSearchPresenterOutput! private var api: GithubAPIProtocol! // Modelを保持する private var githubModels: [GithubModel] init(output: MVPSearchPresenterOutput, api: GithubAPIProtocol = GithubAPI.shared) { self.output = output self.api = api self.githubModels = [] } } ・コード説明③ MVPSearchPresenterInputプロトコルを準拠してViewControllerからの入力を処理します。 Presenterとしては、何でか分からないけど とりあえず入力が来たから処理して出力したって感じです。 Presenterにとっては、検索バーがタップされたとか画面が遷移したとかは関係なく知らなくても良いのです。 更に、入力・出力する相手も関係ないのでテストがしやすくデザインを決める前にも開発が可能です。 extension MVPSearchPresenter: MVPSearchPresenterInput { var numberOfItems: Int { githubModels.count } func item(index: Int) -> GithubModel { githubModels[index] } func search(param: String?) { if let validationError = ParameterValidationError(param: param) { // ViewControllerに任せる output.validation(error: validationError) return } guard let searchText = param else { return } // ViewControllerに任せる output.update(loading: true) // API通信 self.api.get(searchText: searchText) {[weak self] (result) in guard let self = self else{ return } switch result { case .success(let githubModels): // ViewControllerに任せる self.output.update(loading: false) if githubModels.isEmpty { // ViewControllerに任せる self.output.get(error: AppError.emptyApiResponce.error) return } self.githubModels = githubModels // ViewControllerに任せる self.output.update(githubModels: githubModels) case .failure(let error): // ViewControllerに任せる self.output.update(loading: false) self.output.get(error: error) } } } func didSelect(index: Int) { guard let githubUrl = URL(string: githubModels[index].urlStr) else { // ViewControllerに任せる output.get(error: AppError.getApiData.error) return } // ViewControllerに任せる output.showWeb(url: githubUrl) } } ViewController では、ViewControllerのコードです。 ViewControllerの特徴として、 ・Viewに関すること以外は書かない ・ifやfor等といった制御構文が入らない などが挙げられます。 MVPSearchViewController.swift import UIKit final class MVPSearchViewController: UIViewController { @IBOutlet weak private var tableView: UITableView! { didSet { tableView.register(UINib(nibName: TableViewCell.className, bundle: nil), forCellReuseIdentifier: TableViewCell.className) } } @IBOutlet weak private var indicator: UIActivityIndicatorView! private var searchBar = UISearchBar() private var input: MVPSearchPresenterInput! func inject(input: MVPSearchPresenterInput) { self.input = input } override func viewDidLoad() { super.viewDidLoad() self.navigationItem.titleView = searchBar searchBar.delegate = self } } extension MVPSearchViewController: UISearchBarDelegate { func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { input.search(param: searchBar.text) searchBar.resignFirstResponder() } } extension MVPSearchViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { input.numberOfItems } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCell.className, for: indexPath) as! TableViewCell let githubModel = input.item(index: indexPath.item) cell.configure(githubModel: githubModel) return cell } } extension MVPSearchViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) input.didSelect(index: indexPath.row) } } extension MVPSearchViewController: MVPSearchPresenterOutput { func update(loading: Bool) { indicator.animation(isStart: loading) } func update(githubModels: [GithubModel]) { DispatchQueue.main.async { self.searchBar.text = "" self.searchBar.resignFirstResponder() self.tableView.reloadData() } } func validation(error: ParameterValidationError) { Alert.okAlert(vc: self, title: error.message, message: "") } func get(error: Error) { Alert.okAlert(vc: self, title: error.localizedDescription, message: "") } func showWeb(url: URL) { Router.showWeb(url: url, from: self) } } では細かく説明していきます。 ・コード説明① まず、Presenterと繋げるためにinputプロパティとinjectメソッドを用意します。 この部分で外部からPresenterを繋げます。 // このinputがpresenterのこと private var input: MVPSearchPresenterInput! // ここで外部からPresenterを繋げる func inject(input: MVPSearchPresenterInput) { self.input = input } ・コード説明② 今回は、キーボードの検索ボタンを押した時にAPI通信を行い その結果をTableViewに表示するといったアプリとなっています。 なので検索ボタンを押した時にPresenterに知らせないといけません。 extension MVPSearchViewController: UISearchBarDelegate { func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { // Presenterに知らせる input.search(param: searchBar.text) searchBar.resignFirstResponder() } } ・コード説明③ Presenterからの結果をViewControllerが受け取らないといけません。 なので、MVPSearchPresenterOutputプロトコルを準拠して 以下のように結果を受け取れるようにします。 extension MVPSearchViewController: MVPSearchPresenterOutput { func update(loading: Bool) { // インディケータを回すかどうかを決めている indicator.animation(isStart: loading) } func update(githubModels: [GithubModel]) { // TableViewの更新など DispatchQueue.main.async { self.searchBar.text = "" self.searchBar.resignFirstResponder() self.tableView.reloadData() } } func validation(error: ParameterValidationError) { // アラート表示 Alert.okAlert(vc: self, title: error.message, message: "") } func get(error: Error) { Alert.okAlert(vc: self, title: error.localizedDescription, message: "") } func showWeb(url: URL) { // 画面遷移する Router.showWeb(url: url, from: self) } } PresenterとViewControllerを繋げる PresenterとViewControllerを繋げる準備はしましたが、このままだとまだ繋がっていません。 ではどこで繋げていくかというと、画面遷移に関するクラスにて繋げています。 画面遷移に関係あるコードの記述を別ファイルに分けて実装する Router.swift final class Router { static func showMVPSearch(from: UIViewController) { let mvpSearchVC = UIStoryboard.mvpSearchViewController // ここでPresenterとViewControllerを繋げている let presenter = MVPSearchPresenter(output: mvpSearchVC) mvpSearchVC.inject(input: presenter) from.show(next: mvpSearchVC) } } MVPを採用してみて 単純にViewControllerの記述量が減ったのが良きですね。 今回のサンプルアプリでも、その恩恵を得られたので 大規模なアプリだと、もっと効果を感じれそうです。 PresenterはViewがどうなろうと関係ないのでアプリの仕様が分かっていれば すぐに開発できるメリットもありますし、何ならXcodeにも依存していません。 後は、入力・出力先に依存していないのでテストしやすいです。 何か不明な不具合があった時にでも、Presenterは入力・出力先を変えてテスト可能です。 おわりに ソースコードはこちらにあげてます。 何かあれば、コメントして下さると有り難いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AutoLayoutの基礎の基礎の基礎

AutoLayoutとは AutoLayouとは、制約を用いたレイアウト方法です。 制約とは 位置やサイズの決める概念です。 こんなの勉強しなくても、 UIViewクラスのインスタンスプロパティのframeで位置やサイズが決めるられるやん!! って思ったあなたはこの記事を最後まで読むことが決まりました。 (私もそう思ってました。) 話を戻します。 制約は、同一の階層にあるビューや親子関係にあるビューの関係を定義できます。 (ちなみに、frameはスーパービューの座標における位置を定義できます。) 同一の階層にあるビューの関係を定義できるということは、少ないコードで画面サイズの影響を押さえながらレイアウトすることが可能になります。 (frameを用いても画面サイズの影響を抑えることができます。デバイスに応じて画面サイズを取得してif文で条件分岐してframeを毎回決めるということをすれば.......気の遠くなるお話ですね) AutoLayoutエンジン 私ちは、viewに対して制約式を与えることになります。 与えた制約式の連立方程式を解くことでレイアウトを決定します。これを行なっているのがAutoLayoutエンジンです。 (名前めっちゃかっこいい) では具体的に、制約式とはなんなのか?見ていきましょう! 制約式 下の図で、青枠で囲まれているのがViewBlueでオレンジ色のviewがViewOrangeだとしましょう。 viewOrangeのスーパービューがViewBlueです。 制約式は、4種類になります。 1.viewOrange.top=viewBlue.top 2.viewOrange.left=viewBlue.left 3.viewOrange.right=viewBlue.right 4.viewOrange.bottom=0.6 × viewBlue.bottom 制約式を一般化するとy=ax+bになります。 (具体的に、連立方程式を効率的に解くアルゴリズムは、難しくて僕には理解できなかった。) (AutoLayouでわからないことがあったらこの基本部分に戻ってくると理解できるかも。)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む