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:

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: July 20, 2018 | Tested with: Xcode 10 Beta 4

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 : Comparable {
    
        var month : Date
        var headlines : [Headline]
    
    }
  3. Add a function in StoriesTableViewController.swift to compute the first day of the month for a given date using Calendar:

    fileprivate 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: - Table view data source
    
        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. Make the MonthSection type conform to the Comparable protocol to define an ordering by the month:

    struct MonthSection : Comparable {
    
        var month : Date
        var headlines : [Headline]
    
        static func < (lhs: MonthSection, rhs: MonthSection) -> Bool {
        return lhs.month < rhs.month
    }
    
    static func == (lhs: MonthSection, rhs: MonthSection) -> Bool {
        return lhs.month == rhs.month
    }
    
    }
  7. After mapping to the grouped values, also sort the sections:

    self.sections = groups.map(MonthSection.init(month:headlines:)).sorted()
  8. Run the example project and check if the sections are grouped correctly.

  9. Create 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:)).sorted()
    }
    
    }
    
    class StoriesTableViewController: UITableViewController {
    
        // ...
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            self.sections = MonthSection.group(headlines: self.headlines)
        }
    
        // ...
    
    }

Making the code generic

Let’s extract a generic TableSection 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:

    • MonthSectionTableSection
    • monthsectionItem
    • headlinesrowItems
  2. Extract the TableSection type into a separate Swift source file and create a group Helper for it.

  3. Replace the specific types Date and Headline with two generic arguments SectionItem and RowItem.

    struct TableSection <SectionItem, RowItem> : Comparable {
    
        var sectionItem : SectionItem
        var rowItems : [RowItem]
    
        // ...
    }
  4. Require the SectionItem to be conforming to the Comparable and Hashable protocol:

    struct TableSection <SectionItem : Comparable&Hashable, RowItem> : Comparable {
        // ...
    }
  5. Change the group function to take a function that returns a SectionItem for a RowItem. The finished type should look like this (TableSection.swift):

    // Copyright 2018, 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 TableSection<SectionItem : Comparable&Hashable, RowItem> : Comparable {
    
        var sectionItem : SectionItem
        var rowItems : [RowItem]
    
        static func < (lhs: TableSection, rhs: TableSection) -> Bool {
            return lhs.sectionItem < rhs.sectionItem
        }
    
        static func == (lhs: TableSection, rhs: TableSection) -> Bool {
            return lhs.sectionItem == rhs.sectionItem
        }
    
        static func group(rowItems : [RowItem], by criteria : (RowItem) -> SectionItem ) -> [TableSection<SectionItem, RowItem>] {
        let groups = Dictionary(grouping: rowItems, by: criteria)
        return groups.map(TableSection.init(sectionItem:rowItems:)).sorted()
    }
    
    }
  6. Use the generic type for the StoriesTableViewController:

    class StoriesTableViewController: UITableViewController {
    
        // ...
        
        var sections = [TableSection<Date, Headline>]()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            self.sections = TableSection.group(rowItems: self.headlines, by: { (headline) in
        return firstDayOfMonth(date: headline.date)
    })
        }
    
        // ...
    
    }
Btn download 3c20f11b8e Download the finished example project
Btn read 3c0e607615 iOS Developer Blog
Btn subscribe 930758687e Subscribe: Email · Twitter
Btn training bbbdf557d2 Next iOS training: 25. Februar - 01. März 2019, Stuttgart
Btn about 5378472193 About me · Contact