Grouping UITableView cells into sections - Swift Generics by example
This example shows how to group cells UITableView into sections. As an example we'll group the news stories from the UITableViewController tutorial example project by month:
Starting with a plain UITableViewController, we'll use the Dictionary(grouping:by:) API that was added in Xcode 10 to group the rows into sections. Then the code is refactored to a generic type - if you've not written generic types before, this will serve as an introduction on how to create generic types for abstracting common tasks while writing UIKit code.
Tutorial video
Tutorial steps
-
Download NewspaperExample-cell_dates.zip as a starting point. This contains a simple UITableViewController with regular cells, not grouped into sections. Make yourself familiar with the code. If it is not straightforward to you, have a look at the UITableViewController tutorial.
-
Define a struct type to store the Headlines grouped by month in StoriesTableViewController.swift:
struct MonthSection { var month: Date var headlines: [Headline] }
-
Declare a function in StoriesTableViewController.swift to compute the first day of the month for a given date using Calendar:
private func firstDayOfMonth(date: Date) -> Date { let calendar = Calendar.current let components = calendar.dateComponents([.year, .month], from: date) return calendar.date(from: components)! }
-
In StoriesTableViewController, overwrite viewDidLoad to compute the values grouped by date using Dictionary(grouping:by:). Map the result to an Array of MonthSections:
class StoriesTableViewController: UITableViewController { // ... var sections = [MonthSection]() override func viewDidLoad() { super.viewDidLoad() let groups = Dictionary(grouping: self.headlines) { (headline) in return firstDayOfMonth(date: headline.date) } self.sections = groups.map { (key, values) in return MonthSection(month: key, headlines: values) } } // ... }
Hint: As the parameters for the map closure are the same as the initializer of the MonthSection, mapping the sections can be shortened to:
self.sections = groups.map(MonthSection.init(month:headlines:))
-
Update the methods from the UITableViewDataSource protocol to show the values grouped by section:
class StoriesTableViewController: UITableViewController { // ... // MARK: - UITableViewDataSource override func numberOfSections(in tableView: UITableView) -> Int { return self.sections.count } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { let section = self.sections[section] let date = section.month let dateFormatter = DateFormatter() dateFormatter.dateFormat = "MMMM yyyy" return dateFormatter.string(from: date) } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let section = self.sections[section] return section.headlines.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath) let section = self.sections[indexPath.section] let headline = section.headlines[indexPath.row] cell.textLabel?.text = headline.title cell.detailTextLabel?.text = headline.text cell.imageView?.image = UIImage(named: headline.image) return cell } }
-
After mapping to the grouped values, also sort the sections:
override func viewDidLoad() { super.viewDidLoad() let groups = Dictionary(grouping: headlines) { headline in firstDayOfMonth(date: headline.date) } self.sections = groups.map(MonthSection.init(month:headlines:)) self.sections.sort { (lhs, rhs) in lhs.month < rhs.month } }
-
Run the example project and check if the sections are grouped correctly.
-
Extract a static function in the MonthSection type to group the values:
// ... struct MonthSection : Comparable { // ... static func group(headlines : [Headline]) -> [MonthSection] { let groups = Dictionary(grouping: headlines) { (headline) -> Date in return firstDayOfMonth(date: headline.date) } return groups.map(MonthSection.init(month:headlines:)) } } class StoriesTableViewController: UITableViewController { // ... override func viewDidLoad() { super.viewDidLoad() self.sections = MonthSection.group(headlines: self.headlines) } // ... }
-
Optionally, add code to sort the sections by month:
override func viewDidLoad() { super.viewDidLoad() self.sections = MonthSection.group(headlines: self.headlines) self.sections.sort { (lhs, rhs) in lhs.month < rhs.month } }
You can download the example code here: NewspaperExample-grouped_sections_simple.zip
Making the code generic
Let's extract a generic GroupedSection type from the specific MonthSection:
-
Find universal names for the MonthSection type and its fields. Rename everything accordingly using Editor » Refactor » Rename and Editor » Edit all in scope:
- MonthSection → GroupedSection
- month → sectionItem
- headlines → rows
-
Replace the specific types Date and Headline with two generic arguments SectionItem and RowItem.
struct GroupedSection <SectionItem, RowItem> : Comparable { var sectionItem : SectionItem var rows : [RowItem] // ... }
-
Change the group function to take a function that returns a SectionItem for a RowItem:
struct GroupedSection<SectionItem, RowItem> { var sectionItem : SectionItem var rows : [RowItem] static func group(rows : [RowItem], by criteria : (RowItem) -> SectionItem) -> [GroupedSection<SectionItem, RowItem>] { let groups = Dictionary(grouping: rows, by: criteria) return groups.map(GroupedSection.init(sectionItem:rows:)) } }
-
This will cause a type error because the SectionItem needs to implement the Hashable protocol to be usable in a Dictionary - make this requirement explicit by requiring the SectionItem to be conforming to the Hashable protocol:
struct GroupedSection <SectionItem : Hashable, RowItem> : Comparable { // ... }
-
Update the StoriesTableViewController to use the generic type:
class StoriesTableViewController: UITableViewController { // ... var sections = [GroupedSection<Date, Headline>]() override func viewDidLoad() { super.viewDidLoad() self.sections = GroupedSection.group(headlines: self.headlines, by: { firstDayOfMonth(date: $0.date) }) self.sections.sort { lhs, rhs in lhs.sectionItem < rhs.sectionItem } } // ... }
-
Extract the GroupedSection type into a separate Swift source file and create a group Common for it.
The finished type should look like this (GroupedSection.swift):
// Copyright 2018-2019, Ralf Ebert
// License https://opensource.org/licenses/MIT
// License https://creativecommons.org/publicdomain/zero/1.0/
// Source https://www.ralfebert.de/ios-examples/uikit/uitableviewcontroller/grouping-sections/
struct GroupedSection<SectionItem : Hashable, RowItem> {
var sectionItem : SectionItem
var rows : [RowItem]
static func group(headlines : [RowItem], by criteria : (RowItem) -> SectionItem) -> [GroupedSection<SectionItem, RowItem>] {
let groups = Dictionary(grouping: headlines, by: criteria)
return groups.map(GroupedSection.init(sectionItem:rows:))
}
}