こんにちは、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を使用する代わりに、いくつかのクラスに分割します。
- DRHTableViewController:それをUIViewControllerのサブクラスにし、そのサブビューとしてUITableViewを追加します
- DRHTableViewCell: UITableViewCellのサブクラス
- DRHTableViewDataModel:このクラスは、APIリクエストを処理し、データを生成し、委任を使用してDRHTableViewControllerに返します
- DRHTableViewDataModelItem: DRHTableViewCellセルに表示するデータを含む単純なクラス
UITableViewCellから始めましょう
パート1:TableViewCell「シングルビューアプリケーション」として新しいプロジェクトを作成し、標準のViewController.swiftおよびMain.storyboardファイルを削除します。 後で必要なすべてのファイルを段階的に作成します。
最初に、
UITableViewCellのサブクラスを作成し
ます 。 XIBファイルを使用する場合は、「XIBファイルも作成する」オプションをオンにします。
この例では、次のフィールドを持つテーブルセルを使用します。
- アバター画像(ユーザー画像)
- 名前ラベル(ユーザー名)
- 日付ラベル
- 記事タイトル
- 記事のプレビュー
テーブルセルの設計は、このガイドで行うことには影響しないため、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メソッドは、プロジェクトで画像を読み込む方法に依存するため、この記事では説明しません。セルの準備ができたので、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)
tableViewDataSourceと
TableViewDelegateを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に準拠していません」 (タイプ
DRHTableViewControllerは
UITableViewDataSourceプロトコルに準拠していません)。 これは、これらの拡張機能で実装する必要があるいくつかの必須メソッドがあるためです。
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:DataModelDRHDataModelクラスを作成します。
このクラス内で、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) {
ローカル
dataArrayをdataで初期化するとすぐに、テーブルを更新する準備ができました。 ただし、
didRecieveDataUpdateメソッドでこれを行う代わりに、
dataArrayプロパティ
ブラウザを使用します。
fileprivate var dataArray = [DRHTableViewDataModelItem]() { didSet { tableView?.reloadData() } }
didSet内のコードは、
dataArray が初期化された直後、つまり必要なときに実行されます。
以上です! これで、個別に構成されたテーブルセルとデータが初期化された
tableViewのプロトタイプができました。 そして、数千行のコードを持つ
tableViewControllerのクラスはありません。 作成したコードの各ブロックは再利用可能であり、プロジェクト内のどこでも再利用できるため、否定できない利点があります。
便宜上、Githubの次の
リンクで完全なプロジェクトコードを読むことができます。