Iglistkit: Embedded Section Controller does not update.

Created on 15 Apr 2018  路  3Comments  路  Source: Instagram/IGListKit

Issue

One way we use IGListKit at is to show one horizontal section controller, containing cells that scroll horizontally within a collection view that scrolls vertically. We made use of the example provided within this repository, but have encountered one problem. When performUpdates is called on the adapter the first time, everything updated as expected, but a second refresh does not trigger cellForItem within EmbeddedSectionController to be called. Strange enough, after some debugging from our side didUpdate is actually called, but the cells do simply not update. Also when manually scrolling the cells out of the screen to force a refresh the expected data will appear.

The code below was slightly formatted to be shared publicly, but the main structure remains the same as in our tests.

Maybe you could help us to figure out why EmbeddedSectionController does not perform updates on cells once new data is available.

Collection View

/**
 * CollectionView.swift
 * App
 *
 * Created by Dennis Gecaj on 4/15/18.
 *
 * The collection view shows an embedded section.
 *
 * - author: Dennis Gecaj
 * - copyright: 漏 Yello Inc. All rights reserved.
 * - version: 0.1
 */

import UIKit

import IGListKit

class CollectionView: UICollectionView {
  var horizontalSectionData: HorizontalSectionData? {
    didSet {
      adapter.performUpdates(animated: true, completion: nil)
    }
  }

  let adapter = ListAdapter(updater: ListAdapterUpdater(), viewController: nil, workingRangeSize: 0)

  override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
    super.init(frame: frame, collectionViewLayout: layout)

    setUp()
  }

  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)

    setUp()
  }

  func setUp() {
    adapter.collectionView = self
    adapter.dataSource = self
  }
}

//  MARK: - ListAdapterDataSource
extension ContactsCollectionView: ListAdapterDataSource {
  func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
    var objects = [ListDiffable]()
    if let horizontalSectionData = horizontalSectionData,
      discoverProfilesSectionData.embeddedCellData.count > 0 {
      objects.append(horizontalSectionData)
    }
    return objects
  }

  func listAdapter(_ listAdapter: ListAdapter,
                   sectionControllerFor object: Any) -> ListSectionController {
    switch object {
    case is HorizontalSectionData:
      return HorizontalSectionController()

    default:
      return ListSectionController()
    }
  }

  func emptyView(for listAdapter: ListAdapter) -> UIView? {
    return nil
  }
}

HorizontalSectionData

/**
 * HorizontalSectionData.swift
 * App
 *
 * Created by Dennis Gecaj on 4/15/18.
 *
 * The data model for the horizontal section.
 *
 * - author: Dennis Gecaj
 * - copyright: 漏 Yello Inc. All rights reserved.
 * - version: 0.1
 */

import IGListKit

import Interface

class HorizontalSectionData {
  /// An array of embedded cell data.
  var embeddedCellData: [EmbeddedCellData]

  /**
   * Initializes and returns a newly allocated horizontal section object.
   *
   * - Parameter embeddedCellData: An array of embedded cell data.
   *
   * - Returns: A newly allocated horizontal section data object.
   */
  init(embeddedCellData: [EmbeddedCellData]) {
    embeddedCellData = embeddedCellData
  }
}

//  MARK: - ListDiffable
extension DiscoverProfilesSectionData: ListDiffable {
  public func diffIdentifier() -> NSObjectProtocol {
    return "HorizontalSection" as NSObjectProtocol
  }

  func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
    guard let object = object as? HorizontalSectionData else {
      return false
    }

    return embeddedCellData == embeddedCellData
  }
}

HorizontalSectionController

/**
 * HorizontalSectionController.swift
 * App
 *
 * Created by Dennis Gecaj on 4/15/18.
 *
 * The controller of the horizontal section.
 *
 * - author: Dennis Gecaj
 * - copyright: 漏 Yello Inc. All rights reserved.
 * - version: 0.1
 */

import IGListKit

class HorizontalSectionController: ListSectionController {
  var sectionData: HorizontalSectionData?

  let adapter = ListAdapter(updater: ListAdapterUpdater(), viewController: nil, workingRangeSize: 0)

  override init() {
    super.init()

    adapter.dataSource = self
  }

  override func numberOfItems() -> Int {
    return 1
  }

  override func sizeForItem(at index: Int) -> CGSize {
    return HorizontalCell.size
  }

  override func didUpdate(to object: Any) {
    sectionData = object as? HorizontalSectionData
  }

  override func cellForItem(at index: Int) -> UICollectionViewCell {
    guard let cell = collectionContext?.dequeueReusableCell(of: HorizontalCell.self,
                                                      for: self,
                                                      at: index) as? HorizontalCell else {
                                                        fatalError()
    }

    adapter.collectionView = cell.collectionView

    return cell
  }

  override func didSelectItem(at index: Int) { }
}

//  MARK: - ListAdapterDataSource
extension DiscoverProfilesSectionController: ListAdapterDataSource {
  func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
    guard let sectionData = sectionData else {
      return [ListDiffable]()
    }

    return [sectionData]
  }

  func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
    return EmbeddedSectionController()
  }

  func emptyView(for listAdapter: ListAdapter) -> UIView? {
    return nil
  }
}

EmbeddedSectionController

/**
 * EmbeddedSectionController.swift
 * App
 *
 * Created by Dennis Gecaj on 4/15/18.
 *
 * The controller of the embedded section.
 *
 * - author: Dennis Gecaj
 * - copyright: 漏 Yello Inc. All rights reserved.
 * - version: 0.1
 */

import UIKit

import IGListKit

class EmbeddedSectionController: ListSectionController {
  var data: HorizontalSectionData?

  override init() {
    super.init()

    setUp()
  }

  func setUp() {
    minimumInteritemSpacing = 20
    minimumLineSpacing = 20
  }

  override func numberOfItems() -> Int {
    return data?.embeddedCellData.count ?? 0
  }

  override func sizeForItem(at index: Int) -> CGSize {
    return EmbeddedCell.size
  }

  override func didUpdate(to object: Any) {
    self.data = object as? HorizontalSectionData
  }

  override func cellForItem(at index: Int) -> UICollectionViewCell {
    guard let data = data,
      let cell = collectionContext?
        .dequeueReusableCell(withNibName: "EmbeddedCell", bundle: nil,
                             for: self, at: index) as? EmbeddedCell else {
                              fatalError()
    }

    let embeddedCellData = data.embeddedCellData[index]

    cell.nameLabel?.text = embeddedCellData.name

    return cell
  }
}

EmbeddedCellData

struct EmbeddedCellData {
  /// The name as displayed on the cell.
  let name: String
}

//  MARK: - Equatable
extension EmbeddedCellData: Equatable {
  static func == (lhs: EmbeddedCellData, rhs: EmbeddedCellData) -> Bool {
    return lhs.name == rhs.name
  }
}

Most helpful comment

We were able to fix the issue by calling the following within the EmbeddedSectionController.

var data: HorizontalSectionData? {
    didSet {
      collectionContext?.performBatch(animated: true, updates: { batchContext in
        batchContext.reload(self)
      })
    }
  }

All 3 comments

We were able to fix the issue by calling the following within the EmbeddedSectionController.

var data: HorizontalSectionData? {
    didSet {
      collectionContext?.performBatch(animated: true, updates: { batchContext in
        batchContext.reload(self)
      })
    }
  }

@dennisgec You've saved my day! Had the same issue, didUpdate has happened but cells were not reloaded.

What I did and is the following:

I have a class with 20-30 values, but only on a change of 5 of them I actually want a reload of a single cell. So what I did is:

func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
    guard self !== object else { return true }
    guard let object = object as? ClientInspectionDevice else { return false }
    return object.uuid == self.uuid
       && object.manufacturer == self.manufacturer
       && object.floor == self.floor
       && object.area == self.area
       && object.otherZoneAddress == self.otherZoneAddress
       && object.typeOutput == self.typeOutput
}

I defined equality based on those values and then in....

override func didUpdate(to object: Any) {

     if let entry = self.entry, entry.isEqual(toDiffableObject: object as! ClientInspectionDevice) {

         // do nothing

     } else {

         self.entry = object as? ClientInspectionDevice
     }
}

... I'm checking if the desired values of my object have been updated
and if so then I'm calling the following on the didSet observer:

private var entry: ClientInspectionDevice! {

     didSet {

         collectionContext?.performBatch(animated: true, updates: { context in

              context.reload(self)
         })
     }
}

Maybe this gives someone some kind of inspiration.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

lucabartoletti picture lucabartoletti  路  3Comments

Przemyslaw-Wosko picture Przemyslaw-Wosko  路  3Comments

rnystrom picture rnystrom  路  3Comments

joseph-francis picture joseph-francis  路  3Comments

rnystrom picture rnystrom  路  3Comments