IGListKit Tutorial: Better UICollectionViews

In this IGListKit tutorial, you’ll learn to build better, more dynamic UICollectionViews with Instagram’s data-driven framework. By Ron Kliffer.

4.8 (28) · 2 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Adding Messages

JPL engineering is pretty happy that you got the refactor done so quickly, but they really need to establish communication with the stranded astronaut. They’ve asked you to integrate the messaging module ASAP.

Before you add any views, you first need the data.

Open FeedViewController.swift and add a new property to the top of FeedViewController:

let pathfinder = Pathfinder()

PathFinder() acts as a messaging system, and represents the physical Pathfinder rover the astronaut dug up on Mars.

Locate objects(for:) in your ListAdapterDataSource extension and modify the contents to match the following:

var items: [ListDiffable] = pathfinder.messages
items += loader.entries as [ListDiffable]
return items

You might recall that this method provides data source objects to your ListAdapter. The modification here adds the pathfinder.messages to items to provide messages for a new section controller.

Note: You have to cast the entries array to make the Swift compiler happy. The objects already conform to IGListDiffable.

Right-click the SectionControllers group to create a new ListSectionController subclass named MessageSectionController. Add the IGListKit import to the top:

import IGListKit

With the compiler happy, you’ll leave the rest unchanged for now.

Go back to FeedViewController.swift and update listAdapter(_:sectionControllerFor:) in the ListAdapterDataSource extension so it appears as follows:

if object is Message {
  return MessageSectionController()
} else {
  return JournalSectionController()
}

This now returns the new message section controller if the data object is of type Message.

The JPL team wants you to set up MessageSectionController with the following requirements:

  • Receives a Message.
  • Has a bottom inset of 15 points.
  • Returns a single cell sized using the MessageCell.cellSize(width:text:) method.
  • Dequeues and configures a MessageCell using the Message object’s text and user.name values to populate labels.
  • Displays the Message object’s user.name value in all capitals.

Give it a shot! The team drafted up a solution below in case you need help.

[spoiler title=”MessageSectionController”]

import IGListKit

class MessageSectionController: ListSectionController {
  var message: Message!
  
  override init() {
    super.init()
    inset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0)
  }
}

// MARK: - Data Provider
extension MessageSectionController {
  override func numberOfItems() -> Int {
    return 1
  }
  
  override func sizeForItem(at index: Int) -> CGSize {
    guard let context = collectionContext else {
      return .zero
    }
    return MessageCell
      .cellSize(width: context.containerSize.width, text: message.text)
  }
  
  override func cellForItem(at index: Int) -> UICollectionViewCell {
    let cell = collectionContext?
      .dequeueReusableCell(of: MessageCell.self, for: self, at: index) 
        as! MessageCell
    cell.messageLabel.text = message.text
    cell.titleLabel.text = message.user.name.uppercased()
    return cell
  }
  
  override func didUpdate(to object: Any) {
    message = object as? Message
  }  
}

[/spoiler]

Once you’re ready, build and run to see messages integrated into the feed!

IGListKit

Weather on Mars

Your astronaut needs to be able to get the current weather in order to navigate around obstacles like dust storms. JPL built another module that displays the current weather. There’s a lot of information in there though, so they ask that the weather only display when tapped.

IGListKit

Create one last section controller named WeatherSectionController. Start the class off with an initializer and some variables:

import IGListKit

class WeatherSectionController: ListSectionController {
  // 1
  var weather: Weather!
  // 2
  var expanded = false
  
  override init() {
    super.init()
    // 3
    inset = UIEdgeInsets(top: 0, left: 0, bottom: 15, right: 0)
  }
}

What this code does:

  1. This section controller will receive a Weather object in didUpdate(to:).
  2. expanded is a Bool used to track whether the astronaut has expanded the weather section. You initialize it to false so the detail cells are initially collapsed.
  3. Just like the other sections, use a bottom inset of 15 points.

Now add an extension to WeatherSectionController and override three methods:

// MARK: - Data Provider
extension WeatherSectionController {
  // 1
  override func didUpdate(to object: Any) {
    weather = object as? Weather
  }

  // 2
  override func numberOfItems() -> Int {
    return expanded ? 5 : 1
  }
  
  // 3
  override func sizeForItem(at index: Int) -> CGSize {
    guard let context = collectionContext else { 
      return .zero 
    }
    let width = context.containerSize.width
    if index == 0 {
      return CGSize(width: width, height: 70)
    } else {
      return CGSize(width: width, height: 40)
    }
  }
}

Here’s how this works:

  1. In didUpdate(to:), you save the passed Weather object.
  2. If you’re displaying the expanded weather, numberOfItems() returns five cells that will contain different pieces of weather data. If not expanded, you need only a single cell to display a placeholder.
  3. The first cell should be a little larger than the others, as it displays a header. You don’t have to check the state of expanded because that header cell is the first cell in either case.

Next you need to implement cellForItem(at:) to configure the weather cells. Here are some detailed requirements:

  • The first cell should be of type WeatherSummaryCell, others should be WeatherDetailCell.
  • Configure the weather summary cell with cell.setExpanded(_:).
  • Configure four different weather detail cells with the following title and detail labels:
    1. “Sunrise” with weather.sunrise
    2. “Sunset” with weather.sunset
    3. “High” with "\(weather.high) C"
    4. “Low” with "\(weather.low) C"
  1. “Sunrise” with weather.sunrise
  2. “Sunset” with weather.sunset
  3. “High” with "\(weather.high) C"
  4. “Low” with "\(weather.low) C"

Give this cell setup a shot. The solution is just below.

[spoiler title=”WeatherSectionController.cellForItem(at index:)”]

override func cellForItem(at index: Int) -> UICollectionViewCell {
  let cellClass: AnyClass = 
    index == 0 ? WeatherSummaryCell.self : WeatherDetailCell.self
  let cell = collectionContext!
    .dequeueReusableCell(of: cellClass, for: self, at: index)

  if let cell = cell as? WeatherSummaryCell {
    cell.setExpanded(expanded)
  } else if let cell = cell as? WeatherDetailCell {
    let title: String, detail: String
    switch index {
    case 1:
      title = "SUNRISE"
      detail = weather.sunrise
    case 2:
      title = "SUNSET"
      detail = weather.sunset
    case 3:
      title = "HIGH"
      detail = "\(weather.high) C"
    case 4:
      title = "LOW"
      detail = "\(weather.low) C"
    default:
      title = "n/a"
      detail = "n/a"
    }
    cell.titleLabel.text = title
    cell.detailLabel.text = detail
  }
  return cell
}

[/spoiler]

The last thing that you need to do is toggle the section expanded and update the cells when tapped. Override another method from ListSectionController:

override func didSelectItem(at index: Int) {
  collectionContext?.performBatch(animated: true, updates: { batchContext in
    self.expanded.toggle()
    batchContext.reload(self)
  }, completion: nil)
}

performBatch(animated:updates:completion:) batches and performs updates in the section in a single transaction. You can use this whenever the contents or number of cells changes in the section controller. Since you toggle the expansion with numberOfItems(), this will add or remove cells based on the expanded flag.

Return to FeedViewController.swift and add the following near the top of FeedViewController, with the other properties:

let wxScanner = WxScanner()

WxScanner is the model object for weather conditions.

Next, update objects(for:) in the ListAdapterDataSource extension so that it looks like the following:

// 1
var items: [ListDiffable] = [wxScanner.currentWeather]
items += loader.entries as [ListDiffable]
items += pathfinder.messages as [ListDiffable]
// 2
return items.sorted { (left: Any, right: Any) -> Bool in
  guard let 
    left = left as? DateSortable, 
    let right = right as? DateSortable 
    else {
      return false
  }
  return left.date > right.date
}

You’ve updated the data source method to include currentWeather. Here are details on what this does:

  1. Adds the currentWeather to the items array.
  2. All the data conforms to the DataSortable protocol, so this sorts the data using that protocol. This ensures data appears chronologically.

Finally, update listAdapter(_:sectionControllerFor:) to appear as follows:

if object is Message {
  return MessageSectionController()
} else if object is Weather {
  return WeatherSectionController()
} else {
  return JournalSectionController()
}

This returns a WeatherSectionController when a Weather object appears.

Build and run again. You should see the new weather object at the top. Try tapping on the section to expand and contract it.

IGListKit