Введение
UITableView один из самых часто используемых компонентов UIKit. Табличное представление зарекомендовало себя как одно из самых удобных взаимодействий пользователя с контентом представленным на экране смартфона.
На сегодняшний день каждому iOS разработчику необходимо в совершенстве владеть UITableView, знать тонкости и понимать как его адаптировать под разные архитектуры, чтобы использование не вызывало лишних проблем и трудностей.
В этой статье мы поговорим о том, как адаптировать UITableView под архитектуру Model-View-ViewModel (MVVM). Начнём.
Содержание
Введение
Пример
Реализация
Использование
Результат
Вывод
Пример
В качестве примера я реализовал ячейку с кнопкой, картинкой и текстом.
Реализация
Первое, что нам необходимо это создать подкласс AdaptedTableView от UITableView.
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. Стало очень просто работать с секциями, настраивать ячейки, писать меньше шаблонного кода. В то же время осталась возможность настройки таблицы и расширения функционала при необходимости.
Весь код представленный в этой статье можно скачать по этой ссылке.
storoj
дальше не читал
IgorBCC
Я думаю, что все же найдутся люди которым это будет полезно. NLizogubov — посмотрите в сторону TableKit, уверен вам понравится ))
NLizogubov Автор
Обязательно ознакомлюсь с TableKit, спасибо за комментарий)
NLizogubov Автор
storoj Можете описать проблему подробнее? Если есть какая-то конструктивная критика, с большим интересом готов выслушать и исправить свою ошибку)