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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
IGListKit Tutorial: Better UICollectionViews
30 mins
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.
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 theMessage
object’stext
anduser.name
values to populate labels. - Displays the
Message
object’suser.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!
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.
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:
- This section controller will receive a
Weather
object indidUpdate(to:)
. -
expanded
is aBool
used to track whether the astronaut has expanded the weather section. You initialize it tofalse
so the detail cells are initially collapsed. - 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:
- In
didUpdate(to:)
, you save the passedWeather
object. - 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. - 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 beWeatherDetailCell
. - Configure the weather summary cell with
cell.setExpanded(_:)
. - Configure four different weather detail cells with the following title and detail labels:
- “Sunrise” with
weather.sunrise
- “Sunset” with
weather.sunset
- “High” with
"\(weather.high) C"
- “Low” with
"\(weather.low) C"
- “Sunrise” with
- “Sunrise” with
weather.sunrise
- “Sunset” with
weather.sunset
- “High” with
"\(weather.high) C"
- “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:
- Adds the
currentWeather
to the items array. - 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.