Адаптируем UITableView под MVVM

от автора

Введение

UITableView один из самых часто используемых компонентов UIKit. Табличное представление зарекомендовало себя как одно из самых удобных взаимодействий пользователя с контентом представленным на экране смартфона.

На сегодняшний день, каждому iOS разработчику необходимо в совершенстве владеть UITableView, знать тонкости и понимать как его адаптировать под разные архитектуры, чтобы использование не вызывало лишних проблем и трудностей.

В этой статье мы поговорим о том, как адаптировать UITableView под архитектуру Model-View-ViewModel (MVVM). Начнём.

Содержание

  1. Введение

  2. Пример

  3. Реализация

  4. Использование

  5. Результат

  6. Вывод

Пример

В качестве примера я реализовал ячейку с кнопкой, картинкой и текстом.

Реализация

Первым делом создадим подкласс от UITableView и назовем его AdaptedTableView.

class AdaptedTableView: UITableView {      }

Определим метод setup(). Он необходим для конфигурации таблицы. Временно заполним обязательные для реализации методы UITableViewDataSource.

class AdaptedTableView: UITableView {          // MARK: - Public methods          func setup() {         self.dataSource = self     }      }  // MARK: - UITableViewDataSource  extension AdaptedTableView: UITableViewDataSource {          func numberOfSections(in tableView: UITableView) -> Int {         .zero     }          func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {         .zero     }          func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {         UITableViewCell()     }      }

Согласно паттерну MVVM, view владеет viewModel. Создадим абстракцию для входных данных и назовем её AdaptedViewModelInputProtocol. AdaptedSectionViewModelProtocol необходим для описания viewModel секции. AdaptedCellViewModelProtocol служит лишь для полиморфизма подтипов наших viewModels для ячеек.

protocol AdaptedCellViewModelProtocol { }  protocol AdaptedSectionViewModelProtocol {     var cells: [AdaptedCellViewModelProtocol] { get } }  protocol AdaptedViewModelInputProtocol {     var sections: [AdaptedSectionViewModelProtocol] { get } }

Добавляем viewModel. Теперь у нас есть возможность корректно заполнить методы UITableViewDataSource.

class AdaptedTableView: UITableView {          // MARK: - Public properties          var viewModel: AdaptedViewModelInputProtocol?          // MARK: - Public methods          func setup() {         self.dataSource = self     }      }  // MARK: - UITableViewDataSource  extension AdaptedTableView: UITableViewDataSource {          func numberOfSections(in tableView: UITableView) -> Int {         viewModel?.sections.count ?? .zero     }          func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {         viewModel?.sections[section].cells.count ?? .zero     }          func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {         guard let cellViewModel = viewModel?.sections[indexPath.section].cells[indexPath.row] else {             return UITableViewCell()         }              	// TO DO: - Register cell       	// TO DO: - Create cell                  return UITableViewCell()     }      }

На данном этапе с AdaptedTableView почти все готов, однако есть еще пару нерешенных вопросов. Регистрация и переиспользование ячеек. Создадим протокол AdaptedCellProtocol, который будут реализовывать все наши подклассы UITableViewCell, добавим метод register(_ tableView:) и reuse(_ tableView:, for indexPath:).

protocol AdaptedCellProtocol {     static var identifier: String { get }     static var nib: UINib { get }     static func register(_ tableView: UITableView)     static func reuse(_ tableView: UITableView, for indexPath: IndexPath) -> Self }  extension AdaptedCellProtocol {          static var identifier: String {         String(describing: self)     }          static var nib: UINib {         UINib(nibName: identifier, bundle: nil)     }          static func register(_ tableView: UITableView) {         tableView.register(nib, forCellReuseIdentifier: identifier)     }          static func reuse(_ tableView: UITableView, for indexPath: IndexPath) -> Self {         tableView.dequeueReusableCell(withIdentifier: identifier, for: indexPath) as! Self     }      } 

Для порождения ячеек создадим протокол фабричного метода AdaptedCellFactoryProtocol.

protocol AdaptedCellFactoryProtocol {     var cellTypes: [AdaptedCellProtocol.Type] { get }     func generateCell(viewModel: AdaptedCellViewModelProtocol, tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell }

Добавим поле cellFactory и в didSet поместим регистрацию всех ячеек.

class AdaptedTableView: UITableView {          // MARK: - Public properties          var viewModel: AdaptedViewModelInputProtocol?     var cellFactory: AdaptedCellFactoryProtocol? {         didSet {             cellFactory?.cellTypes.forEach({ $0.register(self)})         }     }          ...      }

Исправим метод делегата.

extension AdaptedTableView: UITableViewDataSource {          ...          func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {         guard             let cellFactory = cellFactory,             let cellViewModel = viewModel?.sections[indexPath.section].cells[indexPath.row]         else {             return UITableViewCell()         }                  return cellFactory.generateCell(viewModel: cellViewModel, tableView: tableView, for: indexPath)     }      }

Использование

С необходимы абстракциями на этом все, пора перейти к конкретным реализациям.

1. Ячейка

В качестве примера я создам ячейку с лейблом по центру и viewModel к ней. Реализация ячейки с кнопкой и картинкой.

protocol TextCellViewModelInputProtocol {     var text: String { get } }  typealias TextCellViewModelType = AdaptedCellViewModelProtocol & TextCellViewModelInputProtocol  class TextCellViewModel: TextCellViewModelType {          var text: String          init(text: String) {         self.text = text     }      }  final class TextTableViewCell: UITableViewCell, AdaptedCellProtocol {          // MARK: - IBOutlets          @IBOutlet private weak var label: UILabel!          // MARK: - Public properties          var viewModel: TextCellViewModelInputProtocol? {         didSet {             bindViewModel()         }     }          // MARK: - Private methods          private func bindViewModel() {         label.text = viewModel?.text     }      }

2. Cекция

class AdaptedSectionViewModel: AdaptedSectionViewModelProtocol {          // MARK: - Public properties        var cells: [AdaptedCellViewModelProtocol]          // MARK: - Init          init(cells: [AdaptedCellViewModelProtocol]) {         self.cells = cells     }      }

3. Фабрика

struct MainCellFactory: AdaptedSectionFactoryProtocol {          var cellTypes: [AdaptedCellProtocol.Type] = [         TextTableViewCell.self     ]          func generateCell(viewModel: AdaptedCellViewModelProtocol, tableView: UITableView, for indexPath: IndexPath) -> UITableViewCell {         switch viewModel {         case let viewModel as TextCellViewModelType:             let view = TextTableViewCell.reuse(tableView, for: indexPath)             view.viewModel = viewModel             return view         default:             return UITableViewCell()         }     }      }

Ну и напоследок нам понадобится viewModel самого модуля.

final class MainViewModel: AdaptedSectionViewModelType {          // MARK: - Public properties          var sections: [AdaptedSectionViewModelProtocol]          // MARK: - Init          init() {         self.sections = []                  self.setupMainSection()     }          // MARK: - Private methods          private func setupMainSection() {         let section = AdaptedSectionViewModel(cells: [             TextCellViewModel(text: "Hello!"),             TextCellViewModel(text: "It's UITableView with using MVVM")         ])         sections.append(section)     }      }

Все готово, пора добавить UITableView на ViewController, установив в качестве custom class наш AdaptedTableView.

В реальном проекте, MVVM очень часто используют с каким-то паттерном навигации, это может быть координатор или роутер. В зону ответственности таких объектов входит DI (Dependency Injection) внедрение всех необходимых модулю зависимостей. Так как это тестовый проект, я захардкодил viewModel и cellFactory прямо во ViewController.

class ViewController: UIViewController {          // MARK: - IBOutlets          @IBOutlet weak var tableView: AdaptedTableView! {         didSet {             tableView.viewModel = MainViewModel()             tableView.cellFactory = MainCellFactory()                          tableView.setup()         }     }      }

Результат

Вывод

В итоге мы получили решение, которое позволяет удобно использовать UITableView с MVVM. Стало очень просто работать с секциями, настраивать ячейки, писать меньше шаблонного кода. В то же время осталась возможность настройки таблицы и расширения функционала при необходимости.


Весь код представленный в этой статье можно скачать по этой ссылке.

ссылка на оригинал статьи https://habr.com/ru/post/531446/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *