В предыдущая статья мы начали рассматривать, как избавиться от шаблонного многострочного кода в приложении iOS. В результате у нас сформировалось первоначальное представление о том, какие основные архитектурные сущности, классы и протоколы станут основой разрабатываемого подхода.
В этой статье мы поговорим о том, как мы будем получать данные и показывать их провайдеру.
Он доступен для использования в таблицах и коллекциях.
- Расскажем подробно о провайдере источника данных повторно используемых таблиц.
- Покажем использование на конкретном примере
- Опишем результат с позиции ТВЕРДЫЙ
- Давайте обсудим преимущества и недостатки подхода.
Цель состоит в том, чтобы составные элементы нашего подхода были независимыми и не влияли друг на друга.
Серия статей:
- Общее описание всей схемы
- Источник данных
- Поставщик данных
- Делегат
- Карта соответствия
- наблюдатель
- Коллекции
- .
- Создаем проект приложения типа Tabbed App, в котором удаляем сразу 2 UIViewControllers (First, Second) — как файлы, так и из раскадровки, заменяя их во втором случае на UINavigationControllers
- В контроллерах таблиц, автоматически созданных Xcode, замените UIViewController статическими ячейками.
- Вызываем первую ячейку контроллера таблицы SimpleArchTableViewController и задаем базовый стиль
- Создаем новый UIViewController с таким же именем, класс для него, а также из переименованной ячейки расширяем к нему выход.
Обратимся к протоколу UITableViewDataSource, откуда мы используем 3 метода, необходимых для настройки внешнего вида таблицы.
Класс TableViewDataSource реализует протокол UITableViewDataSource в соответствии с принцип единоличной ответственности .class TableViewDataSource: NSObject { } extension TableViewDataSource: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { <#code#> } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { <#code#> } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { <#code#> } }
Обязанностью этого класса будет возврат массива ячеек, разделенного на секции (на данный момент секция может быть только одна).
Более того, чтобы этот класс можно было использовать повторно, он не должен ничего знать ни о типе возвращаемых ячеек, ни об их идентификаторах, классах и перьях.
Также не следует привязывать его к конкретной реализации модели представления ячейки — это выполнение принцип инверсии зависимостей .
Источник данных Для многократного использования табличного источника данных требуется поставщик.
Он скроет за собой логику преобразования данных, хранящихся в любой возможной форме, в саму структуру массива ячеек, разбитого на секции.
Это необходимо для того, чтобы наш источник данных не зависел от входящего сбора данных.
Также не будет необходимости переписывать его при изменении типа коллекции, хранящей данные.
Еще одним преимуществом будет возможность использовать этот провайдер при работе с UICollectionView (об этом мы поговорим в следующей статье).
Очевидно, что согласно принцип инверсии зависимостей Поставщик данных должен быть закрыт по протоколу, определенному на уровне TableViewDataSource. class TableViewDataSource: NSObject {
let dataProvider: ViewModelDataProvider
override init(dataProvider: ViewModelDataProvider) {
self.dataProvider = dataProvider
}
}
protocol ViewModelDataProvider {
func numberOfSections() -> Int
func numberOfRows(inSection section: Int) -> Int
func itemForRow(atIndexPath indexPath: IndexPath) -> ItemViewModel
}
protocol ItemViewModel {
}
Протокол ItemViewModel здесь нужен, чтобы скрыть конкретную реализацию данных для ячейки.
Отсутствие в нем методов и свойств выяснится чуть позже.
Важно, чтобы метод itemForRow(atIndexPath:) не возвращал необязательное значение, так как в нашей компании мы предпочитаем диагностировать ошибки на ранней стадии, а не заниматься проблемами, которые явно не проявляют себя, стоимость которых только увеличится.
.
Реализуем методы табличного источника данных.
func numberOfSections(in tableView: UITableView) -> Int {
return dataProvider.numberOfSections()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return dataProvider.numberOfRows(inSection: section)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard
let viewModel = dataProvider.itemForRow(atIndexPath: indexPath),
let fabric = cellFabric(viewModel: viewModel)
else {
return UITableViewCell()
}
let cell = fabric.makeCell(for: tableView, at: indexPath, with: viewModel)
return cell
}
Давайте подробнее рассмотрим последнюю функцию.
Она:
- получает модель представления для нужной ячейки от поставщика данных;
- затем выбирает фабрику ячеек, подходящую для модели представления, которая создает соответствующую ячейку, связывая ее с моделью представления;
- результат возвращается в систему.
В этом случае мы получаем все преимущества статической типизации при привязке ячейки к ее модели представления.
За идею и реализацию такого гибкого и удобного механизма хочу поблагодарить своего коллегу @Антонмастер .
Функция выбора фабрики выглядит так: func cellFabric(viewModel: ItemViewModel) -> TableViewCellFabric? {
let viewModelType = type(of: viewModel)
let viewModelTypeString = "\(viewModelType)"
return itemViewModelClassToFabricMapping[viewModelTypeString]
}
В приведенном выше коде вы можете видеть, что, в свою очередь, фабрика для данной модели представления берется ее классом из соответствующего словаря.
Сам словарь представлен ниже.
private lazy var itemViewModelClassToFabricMapping = [String: TableViewCellFabric]()
public func registerCell<Cell>(class: Cell.Type,
identifier: String,
for itemViewModelClass: ItemViewModel.Type)
where Cell: UITableViewCell & Configurable {
let cellFabric = GenericTableViewCellFabric<Cell>(cellIdentifier: identifier)
let itemViewModelTypeString = "\(itemViewModelClass)"
itemViewModelClassToFabricMapping[itemViewModelTypeString] = cellFabric
}
За его заполнение отвечает функция RegisterCell. Общий тип зарегистрированной ячейки Cell является потомком системы UITableViewCell, реализующей общий конфигурируемый протокол.
public protocol Configurable where Self: UIView {
associatedtype ItemViewModel
var viewModel: ItemViewModel? { get set }
}
Протокол просто указывает, что представление можно настроить с использованием модели представления.
В нашем случае общий тип Cell указывает настраиваемую ячейку таблицы.
Фабрики ячеек закрываются по следующему протоколу, который определяет их поведение таким образом, что каждая фабрика может работать только с ячейками с одним предопределенным идентификатором.
protocol TableViewCellFabric {
var cellIdentifier: String { get }
func makeCell(for tableView: UITableView,
at indexPath: IndexPath,
with viewModel: ItemViewModel) -> UITableViewCell
}
Давайте рассмотрим реализацию протокола на примере класса.
class GenericTableViewCellFabric<Cell>: TableViewCellFabric
where Cell: UITableViewCell & Configurable {
let cellIdentifier: String
func makeCell(for tableView: UITableView,
at indexPath: IndexPath,
with viewModel: ItemViewModel) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier,
for: indexPath)
guard
let configurableCell = cell as? Cell,
let viewModel = viewModel as? Cell.ItemViewModel
else {
return cell
}
configurableCell.viewModel = viewModel
return configurableCell
}
init(cellIdentifier: String) {
self.cellIdentifier = cellIdentifier
}
}
Эта фабрика соответствует Рекомендуемый Apple алгоритм работы с таблицами сначала запрашивает в таблице ячейку с ранее указанным идентификатором.
Сначала он преобразуется к общему типу ячейки, описанному выше, и тип модели представления проверяется на соответствие типу, используемому для настройки ячейки.
Если проверки пройдены успешно, ячейка привязывается к переданной модели представления.
Пример использования источника данных Давайте рассмотрим использование источника данных на конкретном примере.
Для этого мы описываем класс ячейки и используем протокол для установки ее модели представления.
protocol TextViewModelProtocol: ItemViewModel {
var text: String { get }
}
class TextTableViewCell: UITableViewCell, Configurable {
var viewModel: TextViewModelProtocol? {
didSet {
textLabel?.
text = viewModel?.
text
}
}
}
class TextViewModel: TextViewModelProtocol {
var text: String
init(text: String) {
self.text = text
}
}
Здесь свойства DidSet ViewModel используются для обновления внешнего вида ячейки.
Протокол Конфигурируемый реализует взаимодействие ячейки и ее модели представления, причем тип последней скрыт, что помогает при мокинге данных во время тестирования, а также соответствует принцип инверсии зависимостей ТВЕРДЫЙ .
Конкретной реализацией протокола ViewModelDataProvider является ArrayDataProvider. Он работает с одномерными массивами и используется для отображения данных в одном разделе UITableView или UICollectionView. class ArrayDataProvider<T: ItemViewModel> {
let array: [T]
init(array: [T]) {
self.array = array
}
}
extension ArrayDataProvider: ViewModelDataProvider {
func numberOfSections() -> Int {
return 1
}
func numberOfRows(inSection section: Int) -> Int {
return array.count
}
func itemForRow(atIndexPath indexPath: IndexPath) -> ItemViewModel? {
guard
indexPath.row >= 0,
indexPath.row < array.count
else {
return nil
}
return array[indexPath.row]
}
}
Собираем все вместе и инициализируем в FirstViewController. private let viewModels = [
TextViewModel(text: "First Cell"),
TextViewModel(text: "Cell #2"),
TextViewModel(text: "This is also a text cell"),
]
private lazy var dataSource: TableViewDataSource = {
let dataProvider = ArrayDataProvider(array: viewModels)
let dataSource = TableViewDataSource(dataProvider: dataProvider)
dataSource.registerCell(class: TextTableViewCell.self,
identifier: "TextTableViewCell",
for: TextViewModel.self)
return dataSource
}()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
tableView.dataSource = dataSource
}
Последний шаг — установить класс и идентификатор единственной ячейки-прототипа таблицы FirstViewController в раскадровке.
Результатом запуска является UITabbarViewController с двумя вкладками.
На данный момент нас интересует только первая ячейка первой вкладки; при нажатии на нее открывается таблица с тремя ячейками, содержащими разный текст.
Графическое представление Опишем результат с позиции ТВЕРДЫЙ :
- Каждый блок на этой диаграмме несет одну-единственную ответственность;
- узкоспециализированные интерфейсы;
- количество связей между блоками минимально;
- каждый блок, кроме TextViewModel, поддерживается только одной сильной ссылкой;
- отсутствуют зависимости нижних компонентов от конкретных реализаций верхних компонентов;
- базовые компоненты зависят от абстракций протокола, описанных на более высоком уровне.
Возможности подхода Для демонстрации возможностей подхода сделаем следующее:
- через раскадровку добавляем еще один прототип ячейки в наш UITableViewController;
- присвойте ему идентификатор и класс DetailedTextTableViewCell соответственно;
- измените стиль с «Базового» на «Правильная детализация»;
- По аналогии мы создадим для него новый класс ячейки и модель представления протокола;
protocol DetailedTextViewModelProtocol: TextViewModel { var detailedText: String { get } } class DetailedTextTableViewCell: UITableViewCell, Configurable { var viewModel: DetailedTextViewModelProtocol? { didSet { textLabel?.
text = viewModel?.
text detailTextLabel?.
text = viewModel?.
detailedText } } }
- Давайте реализуем протокол модели представления на примере ячейки параметра настроек.
В этой ячейке будет отображаться название определенного параметра и его числовое значение:
class ValueSettingViewModel: TextViewModel, DetailedTextViewModelProtocol { var detailedText: String { return String(value) } var value: Int init(parameter: String, value: Int) { self.value = value super.init(text: parameter) } }
- добавляем наши данные в массив данных и прописываем в контроллере соответствие идентификатора вновь созданной ячейки классу вновь созданной модели представления.
var array = [
.
ValueSettingViewModel(parameter: "Size", value: 25),
ValueSettingViewModel(parameter: "Opacity", value: 37),
ValueSettingViewModel(parameter: "Blur", value: 13),
]
dataSource.registerCell(class: DetailedTextTableViewCell.self,
identifier: "DetailedTextTableViewCell",
for: ValueSettingViewModel.self)
Это все шаги, необходимые для добавления ячейки с новым типом представления, который работает с типом данных, отличным от существующего контроллера.
Однако ни одна строчка ранее написанного кода не изменилась — принцип открыт-закрыт .
Добавлен класс, описывающий логику представления данных в ячейке, и класс, представляющий сами данные — принцип единой ответственности .
Недостатки подхода Одним из преимуществ показанного подхода является его простота и наглядность.
Минимальный порог входа, позволяющий сразу начать использовать это решение, — средний.
Разработчику младшего уровня придется улучшить свои базовые знания основных архитектурных шаблонов и языка, чтобы быстро включиться в разработку.
Описанный в статье подход хорошо подходит как для разработки с нуля, так и для поддержки крупных долгосрочных проектов, позволяя сократить кодовую базу.
Для легковесных приложений, не обладающих обширным функционалом, это не лучший вариант. Среди недостатков такого подхода стоит отметить невозможность использования нескольких разных типов представлений для одной и той же модели представления в пределах одной таблицы.
Как это можно решить: заменить текущую реализацию класса соответствия модели представления и фабрики создания соответствующих ячеек на более гибкую реализацию карты соответствия, о которой мы подробно поговорим в будущей статье.
Код, использованный в статье, можно посмотреть здесь .
Теги: #iOS #Разработка для iOS #Разработка мобильных приложений #Анализ и проектирование систем #проектирование и рефакторинг #solid #Swift #разработка для мобильных устройств #разработка для мобильных платформ #разработка для мобильных ОС
-
Нет Необходимости В Swift?
19 Oct, 24 -
Онлайн Психолог - Сбор Требований
19 Oct, 24 -
Выбрасываем Антивирус В Мусорку
19 Oct, 24 -
Как Студент Нашел Ошибку В Яндекс.музыке
19 Oct, 24