TableViewの設計でのMVCパターンの使用

こんにちは、Habr! 開発者Stan Ostrovskiyが2016年10月にMedium.comで公開した記事「iOS Tableview with MVC」の翻訳を紹介します。









アプリケーション例UITableView



この記事では、特定の例を使用して、最も一般的なUITableViewインターフェース要素の1つを設計する際に、一般的なMVCパターンのアプリケーションに慣れることができます。 また、この記事はかなり理解しやすい方法で、アプリケーションを設計する際の基本的なアーキテクチャの原則を理解する機会を提供し、 UITableView要素に慣れる機会を提供します 。 かなりの数の開発者がアプリケーションを作成する際にアーキテクチャ上の決定をしばしば無視するという事実を考慮すると、この記事は初心者開発者と特定の経験を持つプログラマーの両方にとって非常に役立つと思います。 MVCパターンはApple自身によって推進されており、iOS向けの開発時に使用される最も一般的なパターンです。 これは、あらゆるタスクに適していることを意味するものではなく、常に最良の選択であるという意味ではありませんが、MVCを使用すると、アプリケーションのアーキテクチャを一般的に理解するのが最も簡単であり、次に、プロジェクトの目的。 この記事は、コードを構造化し、便利で、再利用可能で、読みやすく、コンパクトにするのに役立ちます。



iOSプロジェクトを開発している場合、最もよく使用されるコンポーネントの1つがUITableViewであることは既に知っています 。 iOS向けの開発をまだ行っていない場合は、いずれにしても、 UITableViewが Youtube、Facebook、Twitter、Mediumなどの多くの最新アプリケーションや、インスタントメッセンジャーの大部分などで使用されていることがわかります。 簡単に言えば、可変数のデータオブジェクトを表示する必要があるたびに、 UITableViewを使用します



この目的のためのもう1つの基本的なコンポーネントはCollectionViewです。これは、TableViewよりも柔軟性があるため、個人的に使用することを好みます。



したがって、プロジェクトにUITableViewを追加します。



通常、最も明白な方法はUITableViewControllerで、 UITableViewは既にすぐに組み込まれます。 その構成は非常に簡単で、データ配列を追加してテーブルセルを作成する必要があります。 いくつかの点を除いて、見た目はシンプルで、私たちが望むように機能します。まず、 UITableViewControllerコードが巨大になり、次に、MVCパターンの概念全体が壊れます。



デザインパターンを扱いたくない場合でも、いずれにしても、数千行で構成されるUITableViewControllerコードを細かく分割することになるでしょう。



モデルとコントローラーの間でデータを転送する方法はいくつかあります。この記事では、委任を使用します。 このアプローチは、明確でモジュール化された再利用可能なコードを提供します。



1つのUITableViewControllerを使用する代わりに、いくつかのクラスに分割します。





UITableViewCellから始めましょう



パート1:TableViewCell



「シングルビューアプリケーション」として新しいプロジェクトを作成し、標準のViewController.swiftおよびMain.storyboardファイルを削除します。 後で必要なすべてのファイルを段階的に作成します。



最初に、 UITableViewCellのサブクラスを作成します 。 XIBファイルを使用する場合は、「XIBファイルも作成する」オプションをオンにします。







この例では、次のフィールドを持つテーブルセルを使用します。



  1. アバター画像(ユーザー画像)
  2. 名前ラベル(ユーザー名)
  3. 日付ラベル
  4. 記事タイトル
  5. 記事のプレビュー


テーブルセルの設計は、このガイドで行うことには影響しないため、Autolayoutは好きなように使用できます。 サブビューごとにアウトレットを作成します。 DRHTableViewCell.swiftファイルは次のようになります。



class DRHTableViewCell: UITableViewCell { @IBOutlet weak var avatarImageView: UIImageView? @IBOutlet weak var authorNameLabel: UILabel? @IBOutlet weak var postDateLabel: UILabel? @IBOutlet weak var titleLabel: UILabel? @IBOutlet weak var previewLabel: UILabel? }
      
      





ご覧のとおり、@ IBOutletのすべてのデフォルト値を「!」で変更しました。 「?」 InterfaceBuilderからUILabelをコードに追加するたびに、最後に変数に「!」が自動的に追加されます。つまり、変数は暗黙的な取得オプションとして宣言されます。 これは、Objective-C APIとの互換性を確保するためですが、強制抽出を使用したくないため、代わりに通常のオプションを使用します。



次に、テーブルセルのすべての要素(ラベル、画像など)を初期化するメソッドを追加する必要があります。 各項目に個別の変数を使用する代わりに、小さなDRHTableViewDataModelItemクラスを作成しましょう。



 class DRHTableViewDataModelItem { var avatarImageURL: String? var authorName: String? var date: String? var title: String? var previewText: String? }
      
      





日付を日付型として保存することをお勧めしますが、簡単にするために、この例では、文字列として保存します。



すべての変数はオプションであるため、デフォルト値について心配することはできません。 少し後でInit()を記述し、 DRHTableViewCell.swiftに戻って次のコードを追加して、テーブルセルのすべての要素を初期化します。



 func configureWithItem(item: DRHTableViewDataModelItem) { // setImageWithURL(url: item.avatarImageURL) authorNameLabel?.text = item.authorName postDateLabel?.text = item.date titleLabel?.text = item.title previewLabel?.text = item.previewText }
      
      





SetImageWithURLメソッドは、プロジェクトで画像を読み込む方法に依存するため、この記事では説明しません。



セルの準備ができたので、TableTableViewに移動できます



パート2:TableView



この例では、ストーリーボードでviewControllerを使用します。 最初に、 UIViewControllerのサブクラスを作成します。







このプロジェクトでは、 UITableViewControllerの代わりにUIViewControllerを使用して、要素のコントロールを展開します。 また、 UITableViewを亜種として使用すると、 Autolayoutを使用して好きなようにテーブルを配置できます。 次に、ストーリーボードファイルを作成し、 DRHTableViewControllerと同じ名前を付けます。 オブジェクトを含むライブラリからViewControllerをドラッグし、クラス名を書き込みます。







UITableViewを追加し、コントローラーの4つのエッジすべてバインドします







最後に、 tableViewアウトレットをDRHTableViewControllerに追加します



 class DRHTableViewController: UIViewController { @IBOutlet weak var tableView: UITableView? }
      
      





DRHTableViewDataModelItemをすでに作成しているため、次のローカル変数をクラスに追加できます。



 fileprivate var dataArray = [DRHTableViewDataModelItem]()
      
      





この変数には、テーブルに表示するデータが格納されます。



ViewControllerクラスでこの配列を初期化しないことに注意してください。これはデータの空の配列にすぎません。 後で委任を使用してデータを入力します。



次に、 viewDidLoadメソッドですべての基本的なtableViewプロパティを設定します。 必要に応じて色とスタイルを調整できます。この例で必ず必要なプロパティはregisterNibのみです。



 tableView?.register(nib: UINib?, forCellReuseIdentifier: String)
      
      





このメソッドを呼び出す前にnibを作成してセルの長く複雑な識別子を入力する代わりに、 DRHTableViewCellクラスのNibプロパティとReuseIdentifierプロパティの両方を作成します



プロジェクト本体では、長くて複雑な識別子の使用を常に避けてください。 他のオプションがない場合は、文字列変数を作成してこの値を割り当てることができます。



DRHTableViewCellを開き、次のコードをクラスの先頭に追加します。



 class DRHMainTableViewCell: UITableViewCell { class var identifier: String { return String(describing: self) } class var nib: UINib { return UINib(nibName: identifier, bundle: nil) } ..... }
      
      





変更を保存し、DRHTableViewControllerに戻ります。 registerNibメソッドの呼び出しは、はるかに簡単になります。



 tableView?.register(DRHTableViewCell.nib, forCellReuseIdentifier: DRHTableViewCell.identifier)
      
      





tableViewDataSourceTableViewDelegateをselfに設定することを忘れないでください。



 override func viewDidLoad() { super.viewDidLoad() tableView?.register(DRHTableViewCell.nib, forCellReuseIdentifier: DRHTableViewCell.identifier) tableView?.delegate = self tableView?.dataSource = self }
      
      





これを行うと、コンパイラーはエラーをスローします:「 タイプDRHTableViewControllerの値をタイプUITableViewDelegateに割り当てることができません」(タイプDRHTableViewControllerの値をUITableViewDelegateに割り当てることはできません)。



UITableViewControllerサブクラスを使用する場合、既に組み込みのデリゲートとデータソースがあります。 UITableViewをUIViewControllerのサブ種として追加する場合、UITableControllerをUITableViewControllerDelegateおよびUITableViewControllerDataSourceプロトコルに自分で実装する必要があります。



このエラーを取り除くには、 DRHTableViewControllerクラスに2つの拡張機能を追加するだけです。



 extension DRHTableViewController: UITableViewDelegate { } extension DRHTableViewController: UITableViewDataSource { }
      
      





その後、別のエラーが表示されます: 「タイプDRHTableViewControllerはプロトコルUITableViewDataSourceに準拠していません」 (タイプDRHTableViewControllerUITableViewDataSourceプロトコルに準拠していません)。 これは、これらの拡張機能で実装する必要があるいくつかの必須メソッドがあるためです。



 extension DRHTableViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { } }
      
      





UITableViewDelegateのすべてのメソッドはオプションであるため、それらをオーバーライドしなくてもエラーは発生しません。 UITableViewDelegateのコマンドボタンをクリックして、使用可能なメソッドを確認します。 最も一般的に使用されるのは、テーブルセルの選択、テーブルセルの高さの設定、および上部と下部のテーブルヘッダーの構成方法です。



ご覧のとおり、上記の2つのメソッドは値を返すはずなので、 戻り値の型がありません」というエラー(戻り値がありません)が再び表示されます。 それを修正しましょう。 まず、セクション内の列数を設定します。データ配列dataArrayを既に宣言しているので、その要素数だけを取得できます。



 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return dataArray.count }
      
      





UITableViewControllerで一般的に使用される別のメソッドnumberOfSectionsInTableViewをオーバーライドしていないことに気づいた人もいるかもしれません。 このメソッドはオプションであり、デフォルト値の1を返します。 この例では、 tableViewにセクションが1つしかないため、このメソッドをオーバーライドする必要はありません。



UITableViewDataSourceを構成する最後の手順は、 cellForRowAtIndexPathメソッドでテーブルセルを設定することです。



 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if let cell = tableView.dequeueReusableCell(withIdentifier: DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell { return cell } return UITableViewCell() }
      
      





行ごとに見てみましょう。



テーブルセルを作成するには、識別子DRHTableViewCellを使用してdequeueReusableCellメソッドを呼び出します。 UITableViewCellを返すため、 UITableViewCellからDRHTableViewCellへのオプションのキャストを使用します。



 let cell = tableView.dequeueReusableCell(withIdentifier: DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell
      
      





次に、オプションを安全に削除し、成功した場合はセルを返します。



 if let cell = tableView.dequeueReusableCell(withIdentifier: DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell { return cell }
      
      





値を抽出できなかった場合、デフォルトのセルをUITableViewCellに返します



 if let cell = tableView.dequeueReusableCell(withIdentifier: DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell { return cell } return UITableViewCell()
      
      





たぶん私たちはまだ何かを忘れましたか? はい、セルをデータで初期化する必要があります。



 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { if let cell = tableView.dequeueReusableCell(withIdentifier: DRHTableViewCell.identifier, for: indexPath) as? DRHTableViewCell { cell.configureWithItem(item: dataArray[indexPath.item]) return cell } return UITableViewCell() }
      
      





これで最終パートの準備ができました。DataSourceを作成してTableViewに接続する必要があります



パート3:DataModel



DRHDataModelクラスを作成します。



このクラス内で、JSONファイルまたはHTTPを使用してデータをリクエストします

クエリまたはローカルデータファイルからのみ。 この記事ではこれに焦点を合わせたいとは思わないので、APIリクエストを既に行っており、AnyObject型のオプションの配列とオプションのエラーエラーを返したと仮定します。



 class DRHTableViewDataModel { func requestData() { // code to request data from API or local JSON file will go here // this two vars were returned from wherever: // var response: [AnyObject]? // var error: Error? if let error = error { // handle error } else if let response = response { // parse response to [DRHTableViewDataModelItem] setDataWithResponse(response: response) } } }
      
      





setDataWithResponseメソッドは、リクエストで受信した配列を使用して、 DRHTableViewDataModelItemから配列を作成します。 requestDataの下に次のコードを追加します。



 private func setDataWithResponse(response: [AnyObject]) { var data = [DRHTableViewDataModelItem]() for item in response { // create DRHTableViewDataModelItem out of AnyObject } }
      
      





記憶のとおりDRHTableViewDataModelの初期化子はまだ作成していません。 それでは、 DRHTableViewDataModelクラスに戻って、初期化するメソッドを追加しましょう。 この場合、辞書[String:String]?でオプションの初期化子を使用しますか?..



 init?(data: [String: String]?) { if let data = data, let avatar = data[“avatarImageURL”], let name = data[“authorName”], let date = data[“date”], let title = data[“title”], let previewText = data[“previewText”] { self.avatarImageURL = avatar self.authorName = name self.date = date self.title = title self.previewText = previewText } else { return nil } }
      
      





辞書にフィールドが存在しない場合、または辞書自体がnilの場合、初期化は失敗します(nilを返します)。



この初期化子を使用して、 DRHTableViewDataModelクラスにsetDataWithResponseメソッドを作成できます。



 private func setDataWithResponse(response: [AnyObject]) { var data = [DRHTableViewDataModelItem]() for item in response { if let drhTableViewDataModelItem = DRHTableViewDataModelItem(data: item as? [String: String]) { data.append(drhTableViewDataModelItem) } } }
      
      





forループを完了すると、 DRHTableViewDataModelItemのすぐに使用可能な配列が作成されます。 この配列をTableViewに転送するにはどうすればよいですか?



パート4:デリゲート



最初に、 DRHTableViewDataModelクラス宣言のすぐ上のDRHTableViewDataModel.swiftファイルにDRHTableViewDataModelDelegateデリゲートプロトコルを作成します。



 protocol DRHTableViewDataModelDelegate: class { }
      
      





このプロトコル内で、2つのメソッドも作成します。



 protocol DRHTableViewDataModelDelegate: class { func didRecieveDataUpdate(data: [DRHTableViewDataModelItem]) func didFailDataUpdateWithError(error: Error) }
      
      





プロトコルのキーワード「class」は、プロトコルの適用範囲をクラスタイプ(構造と列挙を除く)に制限します。 これは、弱いデリゲートリンクを使用する場合に重要です。 デリゲートとデリゲートされたオブジェクトの間に強いリンクのループが作成されないようにする必要があるため、弱いリンクを使用します(以下を参照)



次に、オプションのweak変数をDRHTableViewDataModelクラスに追加します。



 weak var delegate: DRHTableViewDataModelDelegate?
      
      





ここで、デリゲートメソッドを追加する必要があります。 この例では、データリクエストが成功しなかった場合、エラーリクエストを渡す必要があります。リクエストが成功した場合、データの配列を作成します。 エラーハンドラメソッドはrequestDataメソッド内にあります



 class DRHTableViewDataModel { func requestData() { // code to request data from API or local JSON file will go here // this two vars were returned from wherever: // var response: [AnyObject]? // var error: Error? if let error = error { delegate?.didFailDataUpdateWithError(error: error) } else if let response = response { // parse response to [DRHTableViewDataModelItem] setDataWithResponse(response: response) } } }
      
      





最後に、2番目のデリゲートメソッドをsetDataWithResponseメソッドの最後に追加します。



 private func setDataWithResponse(response: [AnyObject]) { var data = [DRHTableViewDataModelItem]() for item in response { if let drhTableViewDataModelItem = DRHTableViewDataModelItem(data: item as? [String: String]) { data.append(drhTableViewDataModelItem) } } delegate?.didRecieveDataUpdate(data: data) }
      
      





これで、データをtableViewに転送する準備ができました。



パート5:データマッピング



DRHTableViewDataModelを使用して、 tableViewにデータを入力できます。 最初に、 DRHTableViewController内にdataModelへのリンクを作成する必要があります。



 private let dataSource = DRHTableViewDataModel()
      
      





次に、データをクエリする必要があります。 これはViewWillAppear内で行い 、ページが開くたびにデータが更新されるようにします。



 override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(true) dataSource.requestData() }
      
      





これは簡単な例なので、viewWillAppearのデータをクエリしています。 実際のアプリケーションでは、これはデータのキャッシュ時間、APIの使用、アプリケーションのロジックなど、多くの要因に依存します。



次に、 ViewDidLoadメソッドでデリゲートをselfに設定します。



 dataSource.delegate = self
      
      





DRHTableViewControllerはまだDRHTableViewDataModelDelegate関数を実装していないため、エラーが再び表示されます。 これを修正するには、ファイルの最後に次のコードを追加します。



 extension DRHTableViewController: DRHTableViewDataModelDelegate { func didFailDataUpdateWithError(error: Error) { } func didRecieveDataUpdate(data: [DRHTableViewDataModelItem]) { } }
      
      





最後に、 didFailDataUpdateWithErrorイベントdidRecieveDataUpdateイベントを処理する必要があります



 extension DRHTableViewController: DRHTableViewDataModelDelegate { func didFailDataUpdateWithError(error: Error) { // handle error case appropriately (display alert, log an error, etc.) } func didRecieveDataUpdate(data: [DRHTableViewDataModelItem]) { dataArray = data } }
      
      





ローカルdataArrayをdataで初期化するとすぐに、テーブルを更新する準備ができました。 ただし、 didRecieveDataUpdateメソッドでこれを行う代わりに、 dataArrayプロパティブラウザを使用します。



 fileprivate var dataArray = [DRHTableViewDataModelItem]() { didSet { tableView?.reloadData() } }
      
      





didSet内のコードは、 dataArray 初期化された直後、つまり必要なときに実行されます。



以上です! これで、個別に構成されたテーブルセルとデータが初期化されたtableViewのプロトタイプができました。 そして、数千行のコードを持つtableViewControllerのクラスはありません。 作成したコードの各ブロックは再利用可能であり、プロジェクト内のどこでも再利用できるため、否定できない利点があります。



便宜上、Githubの次のリンクで完全なプロジェクトコードを読むことができます。



All Articles