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.

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

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 :)) } }

Example project