[Разработка под iOS, Разработка мобильных приложений] Адаптируем UITableView под MVVM

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
05-Дек-2020 18:30


ВведениеUITableView один из самых часто используемых компонентов UIKit. Табличное представление зарекомендовало себя как одно из самых удобных взаимодействий пользователя с контентом представленным на экране смартфона.На сегодняшний день, каждому iOS разработчику необходимо в совершенстве владеть UITableView, знать тонкости и понимать как его адаптировать под разные архитектуры, чтобы использование не вызывало лишних проблем и трудностей.В этой статье мы поговорим о том, как адаптировать UITableView под архитектуру Model-View-ViewModel (MVVM). Начнём.Содержание
  • Введение
  • Пример
  • Реализация
  • Использование
  • Результат
  • Вывод
ПримерВ качестве примера я реализовал ячейку с кнопкой, картинкой и текстом.
РеализацияПервым делом создадим подкласс от 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. Стало очень просто работать с секциями, настраивать ячейки, писать меньше шаблонного кода. В то же время осталась возможность настройки таблицы и расширения функционала при необходимости.Весь код представленный в этой статье можно скачать по этой ссылке.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_razrabotka_pod_ios (Разработка под iOS), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_swift, #_uikit, #_mvvm, #_patterns, #_ios_development, #_mobile_development, #_razrabotka_pod_ios (
Разработка под iOS
)
, #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
)
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 23:46
Часовой пояс: UTC + 5