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

Главная боль разработчика при его использовании - это необходимость постоянно реализовывать его методы UITableViewDataSource и UITableViewDelegate

Даже если вся логика и данные вынесены в отдельные сущности, никуда не деться от вечного копипаста, как минимум, двух методов:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {}

Когда большинство экранов приложения состоит из таблиц, появляется сильное желание реализовать все методы один раз, меняя при этом только начинку.

В данном цикле статей я поделюсь своим решением, к которому пришел при работе над одним из проектов.


Начнем с того, что таблица/цы должны отображать совершенно разные категории элементов, поэтому что напрашивается первым шагом - это создать соответствующий «словарь».

Представим, что в приложении есть два совершенно не связанных типа сущностей Car и Video, которые нам предстоит выводить в ячейках таблиц.

enum CellType {
    case car
    case video
}

Теперь создадим классы, наследники стандартного UITableViewCell, соответствующие каждой из сущностей: CarTableViewCell и VideoTableViewCell.

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

Перед использованием наших ячеек в таблице, саму UITableView нужно будет с ними «познакомить». Для этого нам понадобятся имена их классов и их ReuseIdentifier-ы. Добавим их в качестве свойств в наш словарь.

Помимо этого, каждая категория ячеек, зачастую, имеет собственную высоту. Поэтому, в список CellType, так же добавим свойство height, которое эту высоту и будет возвращать.

Итоговый вариант:

enum CellType {

    case car
    case video

    var cellClass: Any {
        switch self {
        case .car:
            return CarTableViewCell.self
        case .video:
            return VideoTableViewCell.self
        }
    }

    var reuseIdentifier: String {
        switch self {
        case .car:
            return "car_cell"
        case .video:
            return "video_cell"
        }
    }

    var height: CGFloat {
        switch self {
        case .car:
            return 44
        case .video:
            return 60
        }
    }
}

 Следующим шагом будет создание универсальной модели ячейки, которая будет обязательно иметь тип CellType и некоторый контент, в виде чего угодно.

class Cell {
    var type: CellType
    var content: Any?

    init(cellType: CellType) {
        self.type = cellType
    }
}

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

struct SectionHeader {
    var title: String = ""
    var height: CGFloat = 0
    var view: UIView? = nil
}

struct SectionFooter {
    var height: CGFloat = CGFloat.leastNormalMagnitude
    var view: UIView? = UIView()
}

class Section {
    var cells: [Cell] = []
    var headerProperties: SectionHeader = SectionHeader()
    var footerProperties: SectionFooter = SectionFooter()
}


Теперь у нас есть все для того, чтобы «удовлетворить базовые потребности» UITableView. Все что для этого потребуется - это массив секций [Section], каждая секция которого будет хранить массив моделей ячеек [Cell]. Он будет передаваться любую из таблиц на отрисовку.

Создадим протокол, реализуя который, любой объект сможет управлять содержимым таблицы:

protocol TableViewUniversalDelegate: class {
    var dataSource: [Section] { get }
}


Осталось связать сущности Cell и Section со стандартными методами DataSource и Delegate.

Классом, который это сделает пускай будет TableView, унаследованный от UITableView.

class TableView: UITableView {

}

Создадим в нем свойство, указывающее на делегат TableViewUniversalDelegate:

let universalDelegate: TableViewUniversalDelegate

Инициализатор, который будет принимать на вход его, категории используемых ячеек, а так же параметры фрэйма CGRect, которые мы передадим дальше в стандартный инициализатор:

init(universalDelegate: TableViewUniversalDelegate, cellTypes: [CellType], frame: CGRect, style: UITableView.Style) {
    self.universalDelegate = universalDelegate
    super.init(frame: frame, style: style)
    dataSource = self
    delegate = self
    registerCellTypes(cellTypes)
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

И метод, который будет регистрировать полученные категории ячеек:

func registerCellTypes(_ types: [CellType]) {
    for cellType in types {
        let nib = UINib(nibName: String(describing: cellType.cellClass), bundle: nil)
        register(nib, forCellReuseIdentifier: cellType.reuseIdentifier)
    }
}


После чего «раз и навсегда» реализуем самые востребованные методы стандартных протоколов UITableViewDataSource и UITableViewDelegate.

extension TableView: UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return universalDelegate.dataSource.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return universalDelegate.dataSource[section].cells.count
    }

    func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        return universalDelegate.dataSource[section].headerProperties.title
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cellModel = universalDelegate.dataSource[indexPath.section].cells[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: cellModel.type.reuseIdentifier, for: indexPath)
        return cell
    }

}

extension TableView: UITableViewDelegate {

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return universalDelegate.dataSource[indexPath.section].cells[indexPath.row].type.height
    }

    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return universalDelegate.dataSource[section].headerProperties.height
    }

    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        return universalDelegate.dataSource[section].footerProperties.height
    }

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        return universalDelegate.dataSource[section].headerProperties.view
    }

    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        return universalDelegate.dataSource[section].footerProperties.view
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
    }

}


Все готово, осталось проверить работу. Создаем ViewController, делаем его делегатом протокола TableViewUniversalDelegate.

class ViewController: UIViewController, TableViewUniversalDelegate {
    var dataSource: [Section] = []
}

В нем же напишем отдельный метод, который будет генерировать контент и создадим несколько ячеек car и video:

func createData() {
    let sectionCars = Section()
    sectionCars.headerProperties.title = "section cars"
    sectionCars.headerProperties.height = 30
    sectionCars.cells.append(Cell(cellType: .car))
    dataSource.append(sectionCars)

    let sectionVideos = Section()
    sectionVideos.headerProperties.title = "section videos"
    sectionVideos.headerProperties.height = 40
    for _ in 1...5 {
        sectionVideos.cells.append(Cell(cellType: .video))
    }
    dataSource.append(sectionVideos)
}

Вызовем генерацию контента в методе viewDidLoad(), а затем с помощью отложенной инициализации создадим TableView, и разместим его на контроллере.

lazy var tableView: TableView = {
    return TableView(universalDelegate: self, cellTypes: [.car, .video], frame: self.view.bounds, style: .plain)
} ()
 
override func viewDidLoad() {
    super.viewDidLoad()
    createData()
    view.addSubview(tableView)
}


Проверяем. Работает:

Simulator Screen Shot - iPhone XR - 2019-03-20 at 16.12.35.png

Конечно, полученный на данном этапе функционал, не достаточен, для создания полноценных приложений.

Как минимум, необходимо наполнить ячейки таблиц реальным контентом и научить их обрабатывать события.

Об этом пойдет речь во второй части.