Как создавать гибкие списки: обзор динамического UICollectionView – IGListKit

от автора

Коллекции есть во многих мобильных приложениях – например, это могут быть списки публикаций в соцсети, рецепты, формы обратной связи и многое другое. Для их создания часто используют UICollectionView. Для формирования гибкого списка нужно синхронизировать модель данных и представление, но при этом возможны различные сбои.

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

Спасибо за внимание! Пример работы с фреймворком можно посмотреть в нашем репозитории.

Пример в gif

ссылка на оригинал статьи https://habr.com/ru/company/simbirsoft/blog/534350/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *