Grouping UITableView cells into sections - Swift Generics by example

by @ralfebert · updated September 19, 2019

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:

Cell Styles starting point
Cells grouped by section

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.

Last update: September 19, 2019 | Tested with: Xcode 11

Tutorial video

Tutorial steps

  1. 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.

    Cell Styles starting point
  2. Define a struct type to store the Headlines grouped by month in StoriesTableViewController.swift:

    struct MonthSection {
        var month: Date
        var headlines: [Headline]
    }
    
  3. 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)!
    }
    
  4. 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:))
    
  5. 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
        }
    
    }
    
  6. 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 }
    
    }
    
  7. Run the example project and check if the sections are grouped correctly.

  8. 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)
        }
    
        // ...
    
    }
    
  9. 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:

  1. Find universal names for the MonthSection type and its fields. Rename everything accordingly using Editor » Refactor » Rename and Editor » Edit all in scope:

    • MonthSectionGroupedSection
    • monthsectionItem
    • headlinesrows
  2. 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]
    
        // ...
    }
    
  3. 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:))
        }
    
    }
    
  4. 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 {
        // ...
    }
    
  5. 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 }
    
        }
    
        // ...
    
    }
    
  6. 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:))
    }

}