Pull to refresh

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

Reading time5 min
Views6.9K

Введение

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

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

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

Содержание

  1. Введение

  2. Пример

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

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

  5. Результат

  6. Вывод

Пример

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

Реализация

Первое, что нам необходимо это создать подкласс 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. Стало очень просто работать с секциями, настраивать ячейки, писать меньше шаблонного кода. В то же время осталась возможность настройки таблицы и расширения функционала при необходимости.


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

Tags:
Hubs:
+6
Comments9

Articles