[Разработка под iOS, Разработка мобильных приложений] Как создавать гибкие списки: обзор динамического UICollectionView – IGListKit

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
22-Дек-2020 14:32

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

Похожие новости: Теги для поиска: #_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-Ноя 20:59
Часовой пояс: UTC + 5