VIPERおよびUITableView / UICollectionViewと単純なセル

良い一日!



私は最近、悪いMVCアプリケーションをVIPERに翻訳し始めました。 これがVIPERアーキテクチャでの私の最初の経験であり、このアーキテクチャに関する現時点でのインターネット上の情報がほとんどないという事実のために、いくつかの問題に遭遇しました。 VIPERの最も一般的な知識と概念を使用して、テーブルやコレクションなど、自分に最適なスクリーンライティングパターンを推測しました。



単純なセルと複雑なセル



セルを単純なものと複雑なものに分けます。



シンプルセルとは、目的を果たすために、一部のデータ(テキスト、画像)を表示し、ユーザーアクションにシンプルアクションで応答するだけのセルです。



複雑なセルとは、目的を果たすために、複雑なビジネスロジックを持つデータを追加でロードする必要があるセルです。



この記事では、単純なセルを持つテーブルに焦点を当てます。



問題



問題は、セルを何らかの方法で組み立て、何らかの形でそのイベントをリッスンし、適切な場所で実行する必要があることです。



解決策



そもそも、セルを別のモジュールとして作成することを提案する人もいるかもしれませんが、単純なセルについて話すとき、これはそれほど簡単ではなく、特に正当化された解決策ではありません。



例ですべてを分析します。 名前、専門分野、写真、職場がある従業員のリストがあるとします。あなたは彼に手紙を書くか電話することができます。 上記のすべての情報と、対応するアクションに対応するボタンを含むセルの形式で、UITableViewにこのリストを表示したいと思います。

各従業員はテーブルの1つのセクションになり、行にうまく収まる情報の各ブロックはこのセクションのセルになります。



だから私たちが持っているもの:





もちろん、メインモジュールのプレゼンターはイベントを処理する必要があります。 アイデアは次のとおりです。



モジュールのインターアクターは通常の形式でデータを受信し、それをプレゼンターに転送します。 次に、プレゼンターは、Viewが理解できるこのデータからデータを収集する必要があります。このようなデータは、行モデルの配列を含むセクションモデルの配列を取得するためです。 セクションモデルにはプレゼンターのデリゲートがあり、これはイベント処理に必要です。 次に、ボタンのあるセルのモデルには、ボタンのイベントを処理するためのボタンがあります。ボタンのイベントは、ボタンが存在するセクションのモデルによって定義されます。 したがって、セル内のボタンをクリックすると、セクションのデリゲートとしてプレゼンターにアピールが行われ、最終的にすべてを処理するブロックが発生します。



画像



したがって、セルはテーブルの要素であり、これはモジュールのビューです。 私の意見では、そのイベントが同じモジュールのプレゼンターによって処理されるという点で、驚くべきことも間違ったこともありません。 セルとセクションのモデルは、セルの最も原始的なプレゼンターのバリアントと考えることができます。これは、何もロードする必要がなく、作業のためのすべての情報が外部から与えられます。 その場合、セルモジュールは最も単純なモジュールで、ViewとPresenterのみで構成されます。 このような「モジュール」の実装は、通常のモジュールの実装とは少し異なります。私はそれを呼び出しません。



実装は、プロトコルを介したポリモーフィズムの使用に基づきます。



プロトコルから始めましょう。これなしでは、すべてがそれほど美しくなることはありません。



すべての細胞モデルが実装するプロトコル:



protocol CellIdentifiable { var cellIdentifier: String { get } var cellHeight: Float { get } }
      
      





モデルを持つすべてのセルが実装するプロトコル:



 protocol ModelRepresentable { var model: CellIdentifiable? { get set } }
      
      





すべてのセクションモデルが実装するプロトコル:



 protocol SectionRowsRepresentable { var rows: [CellIdentifiable] { get set } }
      
      





次に、必要なセルモデルを作成します。



1.すべてのセルに自動高さがあるため、最初にすべてのモデルの基本クラスを作成します。



 class EmployeeBaseCellModel: CellIdentifiable { let automaticHeight: Float = -1.0 var cellIdentifier: String { return "" } var cellHeight: Float { return automaticHeight } }
      
      





2.従業員の写真、名前、専門分野を表示するセルモデル。



 class EmployeeBaseInfoCellModel: EmployeeBaseCellModel { override var cellIdentifier: String { return "EmployeeBaseInfoCell" } var name: String var specialization: String var imageURL: URL? init(_ employee: Employee) { name = employee.name specialization = employee.specialization imageURL = employee.imageURL } }
      
      





3.従業員の職場を表示するセルモデル



 class EmployeeWorkplaceCellModel: EmployeeBaseCellModel { override var cellIdentifier: String { return "EmployeeWorkplaceCell" } var workplace: String init(_ workplace: String) { self.workplace = workplace } }
      
      





4.ボタンセルモデル



 class ButtonCellModel: EmployeeBaseCellModel { typealias ActionHandler = () -> () override var cellIdentifier: String { return "ButtonCell" } var action: ActionHandler? var title: String init(title: String, action: ActionHandler? = nil) { self.title = title self.action = action } }
      
      





セルモデルが完成しました。 セルクラスを作成します。



1.基本クラス



 class EmployeeBaseCell: UITableViewCell, ModelRepresentable { var model: CellIdentifiable? { didSet { updateViews() } } func updateViews() { } }
      
      





コードからわかるように、セルのUI構成は、モデルが与えられるとすぐに発生します。



2.従業員の基本情報のセルクラス。



 class EmployeeBaseInfoCell: EmployeeBaseCell { @IBOutlet weak var nameLabel: UILabel! @IBOutlet weak var specializationLabel: UILabel! @IBOutlet weak var photoImageView: UIImageView! override func updateViews() { guard let model = model as? EmployeeBaseInfoCellModel else { return } nameLabel.text = model.name specializationLabel.text = model.specialization if let imagePath = model.imageURL?.path { photoImageView.image = UIImage(contentsOfFile: imagePath) } } }
      
      





3.職場を表示するセルのクラス



 class EmployeeWorkplaceCell: EmployeeBaseCell { @IBOutlet weak var workplaceLabel: UILabel! override func updateViews() { guard let model = model as? EmployeeWorkplaceCellModel else { return } workplaceLabel.text = model.workplace } }
      
      





4.ボタンセルクラス



 class ButtonCell: EmployeeBaseCell { @IBOutlet weak var button: UIButton! override func updateViews() { guard let model = model as? ButtonCellModel else { return } button.setTitle(model.title, for: .normal) } @IBAction func buttonAction(_ sender: UIButton) { guard let model = model as? ButtonCellModel else { return } model.action?() } }
      
      





セルで仕上げました。 セクションモデルに移りましょう。



 protocol EmployeeSectionModelDelegate: class { func didTapCall(withPhone phoneNumber: String) func didTapText(withEmail email: String) } class EmployeeSectionModel: SectionRowsRepresentable { var rows: [CellIdentifiable] weak var delegate: EmployeeSectionModelDelegate? init(_ employee: Employee) { rows = [CellIdentifiable]() rows.append(EmployeeBaseInfoCellModel(employee)) rows.append(contentsOf: employee.workplaces.map({ EmployeeWorkplaceCellModel($0) })) let callButtonCellModel = ButtonCellModel(title: "") { [weak self] in self?.delegate?.didTapCall(withPhone: employee.phone) } let textButtonCellModel = ButtonCellModel(title: " ") { [weak self] in self?.delegate?.didTapText(withEmail: employee.email) } rows.append(contentsOf: [callButtonCellModel, textButtonCellModel]) } }
      
      





ここで、セルのアクションがプレゼンターに関連付けられます。



残っている最も簡単なことは、テーブルにデータを表示することです。

これを行うには、最初にテーブル内のセルのプロトタイプを作成し、それらに適切な識別子を与えます。



結果は次のようになります。 すべてのクラスにクラスを付加し、識別子を再利用して、すべてのコンセントを接続する必要があります。



画像



次に、Interactorから受信したデータに基づいてPresenterでセクションを組み立て、表示用のビューセクションの配列を提供します。



これがプレゼンターの外観です。



 class EmployeeListPresenter: EmployeeListModuleInput, EmployeeListViewOutput, EmployeeListInteractorOutput { weak var view: EmployeeListViewInput! var interactor: EmployeeListInteractorInput! var router: EmployeeListRouterInput! func viewDidLoad() { interactor.getEmployees() } func employeesDidReceive(_ employees: [Employee]) { var sections = [EmployeeSectionModel]() employees.forEach({ let section = EmployeeSectionModel($0) section.delegate = self sections.append(section) }) view.updateForSections(sections) } } extension EmployeeListPresenter: EmployeeSectionModelDelegate { func didTapText(withEmail email: String) { print("Will text to \(email)") } func didTapCall(withPhone phoneNumber: String) { print("Will call to \(phoneNumber)") } }
      
      





そして、私たちのビューはとても美しく見えます:



 class EmployeeListViewController: UITableViewController, EmployeeListViewInput { var output: EmployeeListViewOutput! var sections = [EmployeeSectionModel]() override func viewDidLoad() { super.viewDidLoad() output.viewDidLoad() } func updateForSections(_ sections: [EmployeeSectionModel]) { self.sections = sections tableView.reloadData() } } extension EmployeeListViewController { override func numberOfSections(in tableView: UITableView) -> Int { return sections.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return sections[section].rows.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let model = sections[indexPath.section].rows[indexPath.row] let cell = tableView.dequeueReusableCell(withIdentifier: model.cellIdentifier, for: indexPath) as! EmployeeBaseCell cell.model = model return cell } override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return CGFloat(sections[indexPath.section].rows[indexPath.row].cellHeight) } }
      
      





そして、ここに結果があります(私はここには書きませんでしたが、少し美しさをもたらしました):



画像



まとめ



目標の非常に柔軟な実装が得られました。モデルを使用すると、ビューに触れずに小さなコードだけを変更することなく、目的のセルをすばやく削除または追加できます。



ビューを汚染しないように、任意の方法でモデルを拡張できます。 たとえば、特定のセルに対してのみ選択を無効にする必要がある場合は、適切なプロパティをモデルに追加して、上記の方法でセルを構成できます。

誰かがもっと美しく、正確で便利なものを提供する準備ができているなら、これが私の現在の実装です-私は幸せです! 次の記事では、複雑なセルの実装について話そうとします(自分で便利なものを見つけたとき)。



プロジェクトリポジトリへのリンク




All Articles