Коллекции есть во многих мобильных приложениях – например, это могут быть списки публикаций в соцсети, рецепты, формы обратной связи и многое другое. Для их создания часто используют UICollectionView. Для формирования гибкого списка нужно синхронизировать модель данных и представление, но при этом возможны различные сбои.
В статье рассмотрим фреймворк IGListKit, созданный командой разработчиков Instagram для решения описанной выше проблемы. Он позволяет настроить коллекцию с несколькими видами ячеек и переиспользовать их буквально в несколько строк. При этом у разработчика есть возможность инкапсулировать логику фреймворка от основного ViewController. Далее расскажем об особенностях создания динамической коллекции и обработки событий. Обзор может быть полезен как начинающим, так и опытным разработчикам, желающим освоить новый инструмент.
Применение фреймворка IGListKit в общих чертах схоже со стандартной реализацией UICollectionView. При этом у нас есть:
Кроме того, есть вспомогательные классы:
Рассмотрим их использование подробнее.
Для начала нам нужно создать модель, которая представляет собой класс, а не структуру. Эта особенность связана с тем, что IGListKit написан на Objective-C.
Теперь расширим модель протоколом ListDiffable.
ListDiffable позволяет однозначно идентифицировать и сравнивать объекты, чтобы безошибочно автоматически обновлять данные внутри UICollectionView.
Протокол требует реализации двух методов:
Этот метод возвращает уникальный идентификатор модели, используемый для сравнения.
Этот метод служит для сравнения двух моделей между собой.
При работе с IGListKit принято использовать модели для создания и работы каждой из ячеек и SectionController. Эти модели создают по правилам, описанным выше. Пример можно посмотреть в репозитории.
После создания модели ячейки необходимо синхронизировать данные с заполнением самой ячейки. Допустим, у нас уже есть сверстанная ячейка ExpandingCell. Добавим к ней возможность работы с IGListKit и расширим для работы с протоколом ListBindable.
Данный протокол требует реализации метода func bindViewModel(_ viewModel: Any). Этот метод обновляет данные в ячейке.
После того, как мы получаем готовые модели данных и ячейки, мы можем приступить к их использованию и формированию списка. Создадим класс SectionController.
Наш класс наследуется от
Это означает, что для работы с SectionController подойдет любая модель, которая соответствует ListDiffable.
Также нам необходимо расширить SectionController протоколом ListBindingSectionControllerDataSource.
Для соответствия протоколу реализуем 3 метода:
Этот метод формирует массив моделей в порядке вывода в UICollectionView.
Метод возвращает нужную ячейку в соответствии с моделью данных. В этом примере код для подключения ячейки вынесен отдельно, подробнее можно посмотреть в репозитории.
Метод возвращает размер для каждой ячейки.
Подключим в имеющийся ViewController ListAdapter и модель данных, а также заполним ее. ListAdapter позволяет создавать и обновлять UICollectionView с ячейками.
Для корректной работы адаптера необходимо расширить ViewController протоколом ListAdapterDataSource.
Протокол реализует 3 метода:
Метод требует вернуть массив заполненной модели для SectionController.
Этот метод инициализирует нужный нам SectionController.
Возвращает представление, которое отображается, когда ячейки отсутствуют.
На этом можно запустить проект и проверить работу – UICollectionView должен быть сформирован. Также, поскольку в нашей статье мы затронули динамические списки, добавим обработку нажатий на ячейку и отображение вложенной ячейки.
Нам требуется расширить SectionController протоколом ListBindingSectionControllerSelectionDelegate и добавить в инициализаторе соответствие протоколу.
Следующий метод вызывается в случае нажатия по ячейке:
Для обновления модели данных воспользуемся делегатом.
Мы расширим ViewController и теперь при нажатии на ячейку ExpandingCellModel в модели данных Company изменим свойство isOpened. Далее адаптер обновит состояние UICollectionView, и следующий метод из SectionController отрисует новую открывшуюся ячейку:
В статье мы рассмотрели особенности создания динамической коллекции при помощи IGListKit и обработки событий. Хотя мы затронули только часть возможных функций фреймворка, даже эта часть может быть полезна разработчику в следующих ситуациях:
Спасибо за внимание! Пример работы с фреймворком можно посмотреть в нашем репозитории.
В статье рассмотрим фреймворк 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