[Разработка под iOS, Разработка мобильных приложений] Как создавать гибкие списки: обзор динамического UICollectionView – IGListKit
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Коллекции есть во многих мобильных приложениях – например, это могут быть списки публикаций в соцсети, рецепты, формы обратной связи и многое другое. Для их создания часто используют 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
SPL
===========
Источник:
habr.com
===========
Похожие новости:
- [Информационная безопасность, Разработка мобильных приложений, Разработка под Android, Аналитика мобильных приложений] Как правильно идентифицировать Android-устройства
- [Работа с видео, Разработка мобильных приложений, Монетизация мобильных приложений, Медийная реклама, Контент-маркетинг] Разработка приложения для обработки видео: на что обратить внимание и при чем тут зумеры
- [Разработка под iOS, Разработка мобильных приложений, IT-инфраструктура, Разработка под Android, DevOps] Да кто такой этот ваш Mobile DevOps?
- [Разработка мобильных приложений] Kotlin Multiplatform. Работаем с многопоточностью на практике. Ч.2
- [Информационная безопасность, Разработка под iOS, IT-стандарты, Контекстная реклама, IT-компании] «Фонд электронных рубежей» раскритиковал кампанию Facebook против Apple
- [Разработка мобильных приложений] Kotlin Multiplatform. Работаем с многопоточностью на практике. Ч.1
- [Тестирование IT-систем, Разработка мобильных приложений, IT-инфраструктура, Разработка под Android, DevOps] VirtualBox — Запуск Android эмулятора в виртуальной среде для тестирования Android проекта
- [Разработка под iOS] Как мы делаем App Clips?
- [Open source, Разработка мобильных приложений, Flutter] Состояние Flutter на изолятах
- [Программирование, Разработка под iOS, Разработка мобильных приложений, Разработка под Android] Кошелёк Mobile Challenge: итоги конкурса и подробный разбор решений командой разработки
Теги для поиска: #_razrabotka_pod_ios (Разработка под iOS), #_razrabotka_mobilnyh_prilozhenij (Разработка мобильных приложений), #_ios, #_mobilnaja_razrabotka (мобильная разработка), #_mobilnye_prilozhenija (мобильные приложения), #_iglistkit, #_uicollectionview, #_blog_kompanii_simbirsoft (
Блог компании SimbirSoft
), #_razrabotka_pod_ios (
Разработка под iOS
), #_razrabotka_mobilnyh_prilozhenij (
Разработка мобильных приложений
)
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:42
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Коллекции есть во многих мобильных приложениях – например, это могут быть списки публикаций в соцсети, рецепты, формы обратной связи и многое другое. Для их создания часто используют UICollectionView. Для формирования гибкого списка нужно синхронизировать модель данных и представление, но при этом возможны различные сбои. В статье рассмотрим фреймворк IGListKit, созданный командой разработчиков Instagram для решения описанной выше проблемы. Он позволяет настроить коллекцию с несколькими видами ячеек и переиспользовать их буквально в несколько строк. При этом у разработчика есть возможность инкапсулировать логику фреймворка от основного ViewController. Далее расскажем об особенностях создания динамической коллекции и обработки событий. Обзор может быть полезен как начинающим, так и опытным разработчикам, желающим освоить новый инструмент. Как работать с IGListKit Применение фреймворка IGListKit в общих чертах схоже со стандартной реализацией UICollectionView. При этом у нас есть:
Кроме того, есть вспомогательные классы:
Рассмотрим их использование подробнее. Создание модели данных Для начала нам нужно создать модель, которая представляет собой класс, а не структуру. Эта особенность связана с тем, что 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 и обработки событий. Хотя мы затронули только часть возможных функций фреймворка, даже эта часть может быть полезна разработчику в следующих ситуациях:
Спасибо за внимание! Пример работы с фреймворком можно посмотреть в нашем репозитории. Пример в gifSPL=========== Источник: habr.com =========== Похожие новости:
Блог компании SimbirSoft ), #_razrabotka_pod_ios ( Разработка под iOS ), #_razrabotka_mobilnyh_prilozhenij ( Разработка мобильных приложений ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 14:42
Часовой пояс: UTC + 5