Como criar listas flexíveis: uma visão geral do UICollectionView - IGListKit dinâmico

As coleções estão disponíveis em muitos aplicativos móveis - por exemplo, podem ser listas de publicações em redes sociais, receitas, formulários de feedback e muito mais. UICollectionView é freqüentemente usado para criá-los. Para formar uma lista flexível, você precisa sincronizar o modelo de dados e a exibição, mas várias falhas são possíveis.



Neste artigo, consideraremos a estrutura IGListKitcriado pela equipe de desenvolvimento do Instagram para resolver o problema acima. Ele permite que você configure uma coleção com vários tipos de células e as reutilize em apenas algumas linhas. Ao mesmo tempo, o desenvolvedor tem a capacidade de encapsular a lógica do framework do ViewController principal. A seguir, falaremos sobre os detalhes da criação de uma coleção dinâmica e do tratamento de eventos. A revisão pode ser útil para iniciantes e desenvolvedores experientes que desejam dominar uma nova ferramenta.







Como trabalhar com IGListKit



O uso da estrutura IGListKit é amplamente semelhante à implementação UICollectionView padrão. Além disso, temos:



  • modelo de dados;
  • ViewController;
  • células da coleção UICollectionViewCell.


Além disso, existem classes auxiliares:



  • SectionController - responsável pela configuração das células na seção atual;
  • SectionControllerModel - cada seção tem seu próprio modelo de dados;
  • UICollectionViewCellModel - para cada célula, também seu próprio modelo de dados.


Vamos considerar seu uso com mais detalhes.



Criação de um modelo de dados



Primeiro, precisamos criar um modelo, que é uma classe, não uma estrutura. Este recurso é devido ao fato de que IGListKit é escrito em 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
    }
}
      
      





Agora vamos estender o modelo com o protocolo 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 permite que você identifique e compare objetos de maneira única para atualizar automaticamente os dados dentro do UICollectionView sem erros.



O protocolo requer a implementação de dois métodos:



func diffIdentifier() -> NSObjectProtocol
      
      







Este método retorna um identificador exclusivo para o modelo usado para comparação.



func isEqual(toDiffableObject object: ListDiffable?) -> Bool
      
      







Este método é usado para comparar dois modelos entre si.



Ao trabalhar com IGListKit, é comum usar modelos para criar e operar cada uma das células e SectionController. Esses modelos são criados de acordo com as regras descritas acima. Um exemplo pode ser encontrado no repositório .



Sincronizando uma célula com um modelo de dados



Depois de criar o modelo de célula, você precisa sincronizar os dados com o preenchimento da própria célula. Digamos que já tenhamos uma ExpandingCell definida . Vamos adicionar a capacidade de trabalhar com IGListKit e estendê-la para trabalhar com o protocolo 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")
    }
}

      
      





Este protocolo requer a implementação do método func bindViewModel (_ viewModel: Any) . Este método atualiza os dados na célula.



Formando uma lista de células - SectionController



Depois de preparar os modelos de dados e células, podemos começar a usá-los e formar uma lista. Vamos criar uma classe SectionController.



final class InfoSectionController: ListBindingSectionController<ListDiffable> {
 
    weak var delegate: InfoSectionControllerDelegate?
    
    override init() {
        super.init()
        
        dataSource = self
    }
}
      
      





Nossa classe herda de
ListBindingSectionController<ListDiffable>
      
      





Isso significa que qualquer modelo que esteja em conformidade com ListDiffable funcionará com SectionController.



Também precisamos expandir o protocolo 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)
    }
}
      
      







Para cumprir o protocolo, implementamos 3 métodos:



func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, viewModelsFor object: Any) -> [ListDiffable] 

      
      





Este método constrói uma matriz de modelos na ordem em que aparecem no UICollectionView.



func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, cellForViewModel viewModel: Any, at index: Int) -> UICollectionViewCell & ListBindable 

      
      





O método retorna a célula desejada de acordo com o modelo de dados. Neste exemplo, o código para conectar a célula é retirado separadamente, para mais detalhes, consulte o repositório .



func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, sizeForViewModel viewModel: Any, at index: Int) -> CGSize 

      
      





O método retorna o tamanho de cada célula.



Configurando o ViewController



Vamos conectar o ListAdapter e o modelo de dados ao ViewController existente e também preenchê-lo. ListAdapter permite criar e atualizar UICollectionView com células.



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
    }
}
      
      







Para funcionar corretamente, o adaptador é necessário expandir o protocolo do 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
    }
}
      
      





O protocolo implementa 3 métodos:



func objects(for listAdapter: ListAdapter) -> [ListDiffable]
      
      







O método requer o retorno de um array do modelo preenchido para o SectionController.



func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController
      
      







Este método inicializa o SectionController de que precisamos.



func emptyView(for listAdapter: ListAdapter) -> UIView?
      
      







Retorna a visualização que é exibida quando células estão faltando.



Com isso você pode iniciar o projeto e verificar o trabalho - o UICollectionView deve ser formado. Além disso, como tocamos nas listas dinâmicas em nosso artigo, adicionaremos manipulação para cliques em células e exibir uma célula aninhada.



Tratamento de eventos de clique



Precisamos estender SectionController com o protocolo ListBindingSectionControllerSelectionDelegate e adicionar conformidade de protocolo no inicializador.



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)
    }
}
      
      







O método a seguir é chamado quando uma célula é clicada:



func sectionController(_ sectionController: ListBindingSectionController<ListDiffable>, didSelectItemAt index: Int, viewModel: Any) 
      
      







Vamos usar um delegado para atualizar o modelo de dados.



protocol InfoSectionControllerDelegate: class {
    func sectionControllerDidTapField(_ field: ExpandingCellModel)
}
      
      





Vamos estender o ViewController e agora, ao clicar na célula ExpandingCellModel do modelo de dados da empresa , vamos alterar a propriedade isOpened . O adaptador irá então atualizar o estado de UICollectionView, e o seguinte método do SectionController desenhará a nova célula aberta:



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)
    }
}
      
      







Resumindo



Neste artigo, examinamos os recursos de criação de uma coleção dinâmica usando IGListKit e manipulação de eventos. Embora tenhamos tocado apenas em uma parte das funções possíveis do framework, mesmo esta parte pode ser útil para o desenvolvedor nas seguintes situações:



  • para criar listas flexíveis rapidamente;
  • para encapsular a lógica da coleção do ViewController principal, carregando-a;
  • para configurar uma coleção com vários tipos de células e reutilizá-los.


! .



gif





All Articles