How to create flexible lists: an overview of the dynamic UICollectionView - IGListKit

Collections are available in many mobile applications - for example, they can be lists of publications on social networks, recipes, feedback forms, and much more. UICollectionView is often used to create them. To form a flexible list, you need to synchronize the data model and the view, but various failures are possible.



In this article, we will consider the IGListKit framework created by the Instagram dev team to solve the above problem. It allows you to set up a collection with several types of cells and reuse them in just a few lines. At the same time, the developer has the ability to encapsulate the logic of the framework from the main ViewController. Next, we'll talk about the specifics of creating a dynamic collection and handling events. The review can be useful for both beginners and experienced developers who want to master a new tool.







How to work with IGListKit



The use of the IGListKit framework is broadly similar to the standard UICollectionView implementation. Moreover, we have:



  • data model;
  • ViewController;
  • cells of the UICollectionViewCell collection.


In addition, there are helper classes:



  • SectionController - responsible for the configuration of cells in the current section;
  • SectionControllerModel - each section has its own data model;
  • UICollectionViewCellModel - for each cell, also its own data model.


Let's consider their use in more detail.



Creating a data model



First, we need to create a model, which is a class, not a structure. This feature is due to the fact that IGListKit is written in 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
    }
}
      
      





Now let's extend the model with the ListDiffable protocol .



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 allows you to uniquely identify and compare objects in order to automatically update the data within the UICollectionView without error.



The protocol requires the implementation of two methods:



func diffIdentifier() -> NSObjectProtocol
      
      







This method returns a unique identifier for the model used for comparison.



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







This method is used to compare two models with each other.



When working with IGListKit, it's common to use models to create and operate each of the cells and SectionController. These models are created according to the rules described above. An example can be viewed in the repository .



Synchronizing a cell with a data model



After creating the cell model, you need to synchronize the data with the filling of the cell itself. Let's say we already have a laid out ExpandingCell . Let's add the ability to work with IGListKit to it and extend it to work with the ListBindable protocol .



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

      
      





This protocol requires implementation of the func bindViewModel (_ viewModel: Any) method . This method updates the data in the cell.



Forming a list of cells - SectionController



After we get ready data models and cells, we can start using them and forming a list. Let's create a SectionController class.



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





Our class inherits from
ListBindingSectionController<ListDiffable>
      
      





This means that any model that conforms to ListDiffable will work with SectionController.



We also need to expand SectionController protocol 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)
    }
}
      
      







To comply with the protocol, we implement 3 methods:



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

      
      





This method builds an array of models in the order in which they appear in the UICollectionView.



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

      
      





The method returns the desired cell according to the data model. In this example, the code for connecting the cell is taken out separately, for more details, see the repository .



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

      
      





The method returns the size for each cell.



Setting up the ViewController



Let's connect the ListAdapter and data model to the existing ViewController, and also fill it. ListAdapter allows you to create and update UICollectionView with cells.



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







To work correctly, the adapter is necessary to expand the ViewController protocol 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
    }
}
      
      





The protocol implements 3 methods:



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







The method requires returning an array of the filled model for the SectionController.



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







This method initializes the SectionController we need.



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







Returns the view that is displayed when cells are missing.



On this you can start the project and check the work - the UICollectionView should be generated. Also, since we touched on dynamic lists in our article, we'll add handling for cell clicks and displaying a nested cell.



Handling click events



We need to extend the SectionController with the ListBindingSectionControllerSelectionDelegate protocol and add protocol compliance in the initializer.



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







The following method is called when a cell is clicked:



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







Let's use a delegate to update the data model.



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





We will extend the ViewController and now, when clicking on the ExpandingCellModel cell in the Company data model, we will change the isOpened property . The adapter will then update the state of the UICollectionView, and the following method from the SectionController will draw the new opened cell:



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







Summing up



In this article, we examined the features of creating a dynamic collection using IGListKit and event handling. Although we have touched on only a part of the possible functions of the framework, even this part can be useful to the developer in the following situations:



  • to quickly create flexible lists;
  • to encapsulate the collection logic from the main ViewController, thereby loading it;
  • to set up a collection with multiple kinds of cells and reuse them.


! .



gif





All Articles