В статье рассмотрим фреймворк IGListKit, созданный командой разработчиков Instagram для решения описанной выше проблемы. Он позволяет настроить коллекцию с несколькими видами ячеек и переиспользовать их буквально в несколько строк. При этом у разработчика есть возможность инкапсулировать логику фреймворка от основного ViewController. Далее расскажем об особенностях создания динамической коллекции и обработки событий. Обзор может быть полезен как начинающим, так и опытным разработчикам, желающим освоить новый инструмент.
Как работать с IGListKit
Применение фреймворка IGListKit в общих чертах схоже со стандартной реализацией UICollectionView. При этом у нас есть:
- модель данных;
- ViewController;
- ячейки коллекции UICollectionViewCell.
Кроме того, есть вспомогательные классы:
- SectionController – отвечает за конфигурацию ячеек в текущей секции;
- SectionControllerModel – для каждой секции своя модель данных;
- UICollectionViewCellModel – для каждой ячейки, также своя модель данных.
Рассмотрим их использование подробнее.
Создание модели данных
Для начала нам нужно создать модель, которая представляет собой класс, а не структуру. Эта особенность связана с тем, что IGListKit написан на Objective-C.
final class Company { let id: String let title: String let logo: UIImage let logoSymbol: UIImage var isExpanded: Bool = false init(id: String, title: String, logo: UIImage, logoSymbol: UIImage) { self.id = id self.title = title self.logo = logo self.logoSymbol = logoSymbol } }
Теперь расширим модель протоколом ListDiffable.
extension Company: ListDiffable { func diffIdentifier() -> NSObjectProtocol { return id as NSObjectProtocol } func isEqual(toDiffableObject object: ListDiffable?) -> Bool { guard let object = object as? Company else { return false } return id == object.id } }
ListDiffable позволяет однозначно идентифицировать и сравнивать объекты, чтобы безошибочно автоматически обновлять данные внутри UICollectionView.
Протокол требует реализации двух методов:
func diffIdentifier() -> NSObjectProtocol
Этот метод возвращает уникальный идентификатор модели, используемый для сравнения.
func isEqual(toDiffableObject object: ListDiffable?) -> Bool
Этот метод служит для сравнения двух моделей между собой.
При работе с IGListKit принято использовать модели для создания и работы каждой из ячеек и SectionController. Эти модели создают по правилам, описанным выше. Пример можно посмотреть в репозитории.
Синхронизация ячейки с моделью данных
После создания модели ячейки необходимо синхронизировать данные с заполнением самой ячейки. Допустим, у нас уже есть сверстанная ячейка ExpandingCell. Добавим к ней возможность работы с IGListKit и расширим для работы с протоколом ListBindable.
extension ExpandingCell: ListBindable { func bindViewModel(_ viewModel: Any) { guard let model = viewModel as? ExpandingCellModel else { return } logoImageView.image = model.logo titleLable.text = model.title upDownImageView.image = model.isExpanded ? UIImage(named: "up") : UIImage(named: "down") } }
Данный протокол требует реализации метода func bindViewModel(_ viewModel: Any). Этот метод обновляет данные в ячейке.
Формируем список ячеек – SectionController
После того, как мы получаем готовые модели данных и ячейки, мы можем приступить к их использованию и формированию списка. Создадим класс SectionController.
final class InfoSectionController: ListBindingSectionController<ListDiffable> { weak var delegate: InfoSectionControllerDelegate? override init() { super.init() dataSource = self } }
Наш класс наследуется от
ListBindingSectionController<ListDiffable>
Это означает, что для работы с SectionController подойдет любая модель, которая соответствует ListDiffable.
Также нам необходимо расширить SectionController протоколом ListBindingSectionControllerDataSource.
extension InfoSectionController: ListBindingSectionControllerDataSource { func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, viewModelsFor object: Any) -> [ListDiffable] { guard let sectionModel = object as? InfoSectionModel else { return [] } var models = [ListDiffable]() for item in sectionModel.companies { models.append( ExpandingCellModel( identifier: item.id, isExpanded: item.isExpanded, title: item.title, logo: item.logoSymbol ) ) if item.isExpanded { models.append( ImageCellModel(logo: item.logo) ) } } return models } func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, cellForViewModel viewModel: Any, at index: Int) -> UICollectionViewCell & ListBindable { let cell = self.cell(for: viewModel, at: index) return cell } func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, sizeForViewModel viewModel: Any, at index: Int) -> CGSize { let width = collectionContext?.containerSize.width ?? 0 var height: CGFloat switch viewModel { case is ExpandingCellModel: height = 60 case is ImageCellModel: height = 70 default: height = 0 } return CGSize(width: width, height: height) } }
Для соответствия протоколу реализуем 3 метода:
func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, viewModelsFor object: Any) -> [ListDiffable]
Этот метод формирует массив моделей в порядке вывода в UICollectionView.
func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, cellForViewModel viewModel: Any, at index: Int) -> UICollectionViewCell & ListBindable
Метод возвращает нужную ячейку в соответствии с моделью данных. В этом примере код для подключения ячейки вынесен отдельно, подробнее можно посмотреть в репозитории.
func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, sizeForViewModel viewModel: Any, at index: Int) -> CGSize
Метод возвращает размер для каждой ячейки.
Настраиваем ViewController
Подключим в имеющийся ViewController ListAdapter и модель данных, а также заполним ее. ListAdapter позволяет создавать и обновлять UICollectionView с ячейками.
class ViewController: UIViewController { var companies: [Company] private lazy var adapter = { ListAdapter(updater: ListAdapterUpdater(), viewController: self) }() required init?(coder: NSCoder) { self.companies = [ Company( id: "ss", title: "SimbirSoft", logo: UIImage(named: "ss_text")!, logoSymbol: UIImage(named: "ss_symbol")! ), Company( id: "mobile-ss", title: "mobile SimbirSoft", logo: UIImage(named: "mobile_text")!, logoSymbol: UIImage(named: "mobile_symbol")! ) ] super.init(coder: coder) } override func viewDidLoad() { super.viewDidLoad() configureCollectionView() } private func configureCollectionView() { adapter.collectionView = collectionView adapter.dataSource = self } }
Для корректной работы адаптера необходимо расширить ViewController протоколом ListAdapterDataSource.
extension ViewController: ListAdapterDataSource { func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return [ InfoSectionModel(companies: companies) ] } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { let sectionController = InfoSectionController() return sectionController } func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } }
Протокол реализует 3 метода:
func objects(for listAdapter: ListAdapter) -> [ListDiffable]
Метод требует вернуть массив заполненной модели для SectionController.
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController
Этот метод инициализирует нужный нам SectionController.
func emptyView(for listAdapter: ListAdapter) -> UIView?
Возвращает представление, которое отображается, когда ячейки отсутствуют.
На этом можно запустить проект и проверить работу – UICollectionView должен быть сформирован. Также, поскольку в нашей статье мы затронули динамические списки, добавим обработку нажатий на ячейку и отображение вложенной ячейки.
Обработка событий нажатия
Нам требуется расширить SectionController протоколом ListBindingSectionControllerSelectionDelegate и добавить в инициализаторе соответствие протоколу.
dataSource = self extension InfoSectionController: ListBindingSectionControllerSelectionDelegate { func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, didSelectItemAt index: Int, viewModel: Any) { guard let cellModel = viewModel as? ExpandingCellModel else { return } delegate?.sectionControllerDidTapField(cellModel) } }
Следующий метод вызывается в случае нажатия по ячейке:
func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, didSelectItemAt index: Int, viewModel: Any)
Для обновления модели данных воспользуемся делегатом.
protocol InfoSectionControllerDelegate: class { func sectionControllerDidTapField(_ field: ExpandingCellModel) }
Мы расширим ViewController и теперь при нажатии на ячейку ExpandingCellModel в модели данных Company изменим свойство isOpened. Далее адаптер обновит состояние UICollectionView, и следующий метод из SectionController отрисует новую открывшуюся ячейку:
func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, viewModelsFor object: Any) -> [ListDiffable]
extension ViewController: InfoSectionControllerDelegate { func sectionControllerDidTapField(_ field: ExpandingCellModel) { guard let company = companies.first(where: { $0.id == field.identifier }) else { return } company.isExpanded.toggle() adapter.performUpdates(animated: true, completion: nil) } }
Подводя итоги
В статье мы рассмотрели особенности создания динамической коллекции при помощи IGListKit и обработки событий. Хотя мы затронули только часть возможных функций фреймворка, даже эта часть может быть полезна разработчику в следующих ситуациях:
- чтобы быстро создавать гибкие списки;
- чтобы инкапсулировать логику коллекции от основного ViewController, тем самым загрузив его;
- чтобы настроить коллекцию с несколькими видами ячеек и переиспользовать их.
Спасибо за внимание! Пример работы с фреймворком можно посмотреть в нашем репозитории.
ссылка на оригинал статьи https://habr.com/ru/company/simbirsoft/blog/534350/

Добавить комментарий