Skip to content

UITableView and Custom UITableViewCells in UIKit

UITableView is a UIKit class that displays a scrollable list of data in a single-column format, widely used for lists, settings, or feeds in iOS apps. As a subclass of UIScrollView, it relies on data source and delegate protocols to manage content and interactions. UITableViewCell represents individual rows, and custom cells enable tailored layouts. This document covers the key properties, methods, and usage of UITableView, including custom UITableViewCells, with sections on SwiftUI integration, editing, swipe actions, diffable data sources, multiple sections, UITableViewController, and pull-to-refresh, along with examples and best practices.

Overview of UITableView

UITableView organizes data into sections and rows, supporting scrolling, selection, editing, and animations. It uses UITableViewDataSource to provide content and UITableViewDelegate to handle user interactions. Reusable cells improve performance, and custom cells allow flexible designs.

Creating a UITableView

You can create a UITableView programmatically or via Interface Builder.

Programmatic Example:

swift
import UIKit

let tableView = UITableView()
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
view.addSubview(tableView)

Key Properties of UITableView

PropertyTypeDescription
dataSourceUITableViewDataSource?Provides data (e.g., number of rows, cells).
delegateUITableViewDelegate?Handles interactions and cell customization.
rowHeightCGFloatDefault row height (use .automaticDimension for dynamic sizing).
estimatedRowHeightCGFloatEstimated height for dynamic sizing.
separatorStyleUITableViewCell.SeparatorStyleSeparator style (e.g., .singleLine, .none).
allowsSelectionBoolEnables/disables row selection.
isEditingBoolToggles editing mode.
refreshControlUIRefreshControl?Pull-to-refresh control.

Example: Configuring Properties:

swift
tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 44
tableView.separatorStyle = .singleLine
tableView.allowsSelection = true

Key Methods of UITableView

MethodDescription
register(_:forCellReuseIdentifier:)Registers a cell class or nib for reuse.
dequeueReusableCell(withIdentifier:for:)Dequeues a reusable cell.
reloadData()Reloads all table data.
insertRows(at:with:)Inserts rows with animation.
deleteRows(at:with:)Deletes rows with animation.
selectRow(at:animated:scrollPosition:)Selects a row programmatically.

UITableViewDataSource Protocol

MethodDescription
tableView(_:numberOfRowsInSection:)Returns the number of rows in a section.
tableView(_:cellForRowAt:)Returns a configured cell for a row.
numberOfSections(in:)Returns the number of sections (optional).

UITableViewDelegate Protocol

MethodDescription
tableView(_:didSelectRowAt:)Handles row selection.
tableView(_:heightForRowAt:)Returns custom row height.
tableView(_:trailingSwipeActionsConfigurationForRowAt:)Configures trailing swipe actions.

Creating Custom UITableViewCells

Custom cells enable unique layouts with labels, images, or other views, created programmatically or via nibs/storyboards.

Programmatic Custom Cell

swift
class CustomTableViewCell: UITableViewCell {
    static let identifier = "CustomCell"
    
    let titleLabel = UILabel()
    let subtitleLabel = UILabel()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupViews() {
        titleLabel.font = .systemFont(ofSize: 16, weight: .bold)
        subtitleLabel.font = .systemFont(ofSize: 14)
        subtitleLabel.textColor = .systemGray
        
        let stackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
        stackView.axis = .vertical
        stackView.spacing = 4
        contentView.addSubview(stackView)
        
        stackView.snp.makeConstraints { make in
            make.edges.equalTo(contentView).inset(16)
        }
    }
    
    func configure(title: String, subtitle: String) {
        titleLabel.text = title
        subtitleLabel.text = subtitle
    }
}

Register and Use:

swift
tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.identifier)

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier, for: indexPath) as! CustomTableViewCell
    cell.configure(title: "Item \(indexPath.row)", subtitle: "Details")
    return cell
}

Creating Custom Table Cells Using SwiftUI

SwiftUI views can be hosted in UITableViewCell using UIHostingController for hybrid UIKit/SwiftUI apps.

SwiftUI Cell View

swift
import SwiftUI

struct CustomCellView: View {
    let title: String
    let subtitle: String
    
    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(title)
                .font(.system(size: 16, weight: .bold))
            Text(subtitle)
                .font(.system(size: 14))
                .foregroundColor(.gray)
        }
        .padding(16)
    }
}

Hosting in UITableViewCell

swift
class SwiftUICustomCell: UITableViewCell {
    static let identifier = "SwiftUICell"
    private var hostingController: UIHostingController<CustomCellView>?
    
    func configure(title: String, subtitle: String) {
        if hostingController == nil {
            let contentView = CustomCellView(title: title, subtitle: subtitle)
            hostingController = UIHostingController(rootView: contentView)
            hostingController!.view.translatesAutoresizingMaskIntoConstraints = false
            self.contentView.addSubview(hostingController!.view)
            
            hostingController!.view.snp.makeConstraints { make in
                make.edges.equalTo(contentView)
            }
        } else {
            hostingController!.rootView = CustomCellView(title: title, subtitle: subtitle)
        }
    }
    
    override func prepareForReuse() {
        super.prepareForReuse()
        hostingController?.view.removeFromSuperview()
        hostingController = nil
    }
}

Register and Use:

swift
tableView.register(SwiftUICustomCell.self, forCellReuseIdentifier: SwiftUICustomCell.identifier)

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: SwiftUICustomCell.identifier, for: indexPath) as! SwiftUICustomCell
    cell.configure(title: "SwiftUI Item \(indexPath.row)", subtitle: "SwiftUI Details")
    return cell
}

Inserting and Removing Rows from a Table

UITableView supports animated row insertion and deletion using insertRows and deleteRows.

Example:

swift
var data = ["Item 1", "Item 2", "Item 3"]

func addRow() {
    data.append("Item \(data.count + 1)")
    let indexPath = IndexPath(row: data.count - 1, section: 0)
    tableView.insertRows(at: [indexPath], with: .automatic)
}

func removeRow(at index: Int) {
    data.remove(at: index)
    let indexPath = IndexPath(row: index, section: 0)
    tableView.deleteRows(at: [indexPath], with: .fade)
}

Batch Updates:

swift
tableView.performBatchUpdates {
    data.insert("New Item", at: 0)
    tableView.insertRows(at: [IndexPath(row: 0, section: 0)], with: .top)
    if data.count > 1 {
        data.remove(at: 1)
        tableView.deleteRows(at: [IndexPath(row: 1, section: 0)], with: .fade)
    }
}

Reloading Data in a Table and Selecting Rows of a Table

Reloading Data

Use reloadData() to refresh the entire table or reloadRows(at:with:) for specific rows.

Example:

swift
func refreshData() {
    data = ["Updated Item 1", "Updated Item 2"]
    tableView.reloadData()
}

func updateRow(at index: Int) {
    data[index] = "Updated Item \(index)"
    tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}

Selecting Rows

Handle selection via tableView(_:didSelectRowAt:) and programmatically select rows with selectRow.

Example:

swift
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    print("Selected: \(data[indexPath.row])")
    tableView.deselectRow(at: indexPath, animated: true)
}

func selectRowProgrammatically(at index: Int) {
    let indexPath = IndexPath(row: index, section: 0)
    tableView.selectRow(at: indexPath, animated: true, scrollPosition: .middle)
}

Putting a UITableView Into Edit Mode

Enable editing for row deletion, reordering, or insertion.

Example:

swift
// Enable editing
tableView.setEditing(true, animated: true)

// Toggle edit mode with a button
@objc func toggleEditMode() {
    tableView.setEditing(!tableView.isEditing, animated: true)
}

// Allow deletion
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    return true
}

func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    if editingStyle == .delete {
        data.remove(at: indexPath.row)
        tableView.deleteRows(at: [indexPath], with: .fade)
    }
}

// Allow reordering
func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
    return true
}

func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
    let item = data.remove(at: sourceIndexPath.row)
    data.insert(item, at: destinationIndexPath.row)
}

Swipe Actions on UITableViewCells

Add leading or trailing swipe actions for quick interactions.

Example: Trailing Swipe Actions:

swift
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
    let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { _, _, completion in
        self.data.remove(at: indexPath.row)
        tableView.deleteRows(at: [indexPath], with: .fade)
        completion(true)
    }
    
    let favoriteAction = UIContextualAction(style: .normal, title: "Favorite") { _, _, completion in
        print("Favorited: \(self.data[indexPath.row])")
        completion(true)
    }
    favoriteAction.backgroundColor = .systemYellow
    
    return UISwipeActionsConfiguration(actions: [deleteAction, favoriteAction])
}

Leading Swipe Actions:

swift
func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
    let markAction = UIContextualAction(style: .normal, title: "Mark") { _, _, completion in
        print("Marked: \(self.data[indexPath.row])")
        completion(true)
    }
    markAction.backgroundColor = .systemBlue
    return UISwipeActionsConfiguration(actions: [markAction])
}

UITableViewDiffableDataSource

Introduced in iOS 13, UITableViewDiffableDataSource simplifies data management with type-safe, animated updates.

Example:

swift
enum Section { case main }
typealias DataSource = UITableViewDiffableDataSource<Section, String>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, String>

var dataSource: DataSource!

private func configureDataSource() {
    dataSource = DataSource(tableView: tableView) { tableView, indexPath, item in
        let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier, for: indexPath) as! CustomTableViewCell
        cell.configure(title: item, subtitle: "Details")
        return cell
    }
}

private func applySnapshot(data: [String], animatingDifferences: Bool = true) {
    var snapshot = Snapshot()
    snapshot.appendSections([.main])
    snapshot.appendItems(data)
    dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}

Support Multiple Sections In Tables Using a Diffable Data Source

Diffable data sources handle multiple sections with unique identifiers.

Example:

swift
enum Section: Hashable {
    case featured
    case regular
}

struct Item: Hashable {
    let id = UUID()
    let title: String
}

typealias DataSource = UITableViewDiffableDataSource<Section, Item>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>

var dataSource: DataSource!
var featuredItems: [Item] = [Item(title: "Featured 1"), Item(title: "Featured 2")]
var regularItems: [Item] = [Item(title: "Regular 1"), Item(title: "Regular 2")]

private func configureDataSource() {
    dataSource = DataSource(tableView: tableView) { tableView, indexPath, item in
        let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier, for: indexPath) as! CustomTableViewCell
        cell.configure(title: item.title, subtitle: "Details")
        return cell
    }
    
    // Configure headers
    dataSource.supplementaryViewProvider = { tableView, kind, indexPath in
        let header = tableView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! HeaderView
        header.configure(with: indexPath.section == 0 ? "Featured" : "Regular")
        return header
    }
}

private func applySnapshot(animatingDifferences: Bool = true) {
    var snapshot = Snapshot()
    snapshot.appendSections([.featured, .regular])
    snapshot.appendItems(featuredItems, toSection: .featured)
    snapshot.appendItems(regularItems, toSection: .regular)
    dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
}

Header View:

swift
class HeaderView: UITableViewHeaderFooterView {
    static let identifier = "Header"
    let label = UILabel()
    
    override init(reuseIdentifier: String?) {
        super.init(reuseIdentifier: reuseIdentifier)
        label.font = .systemFont(ofSize: 18, weight: .bold)
        contentView.addSubview(label)
        label.snp.makeConstraints { make in
            make.edges.equalTo(contentView).inset(8)
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configure(with text: String) {
        label.text = text
    }
}

Support Multiple Sections in a UITableView the Old Way

Before iOS 13, multiple sections were managed manually with UITableViewDataSource methods.

Example:

swift
var featuredItems = ["Featured 1", "Featured 2"]
var regularItems = ["Regular 1", "Regular 2"]

func numberOfSections(in tableView: UITableView) -> Int {
    return 2
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return section == 0 ? featuredItems.count : regularItems.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier, for: indexPath) as! CustomTableViewCell
    let item = indexPath.section == 0 ? featuredItems[indexPath.row] : regularItems[indexPath.row]
    cell.configure(title: item, subtitle: "Details")
    return cell
}

func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
    return section == 0 ? "Featured" : "Regular"
}

UITableViewController

UITableViewController is a UIViewController subclass that manages a UITableView, automatically setting itself as the table’s data source and delegate. It simplifies table view setup but is less flexible than embedding a UITableView in a UIViewController.

Example:

swift
class TableViewController: UITableViewController {
    var data = ["Item 1", "Item 2", "Item 3"]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.identifier)
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 44
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return data.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier, for: indexPath) as! CustomTableViewCell
        cell.configure(title: data[indexPath.row], subtitle: "Details")
        return cell
    }
}
  • Pros: Simplified setup, built-in table view management.
  • Cons: Less flexible for complex layouts or multiple views.
  • Use Case: Simple table-based apps.

Implementing Pull to Refresh in a UITableViewController and UITableView (Old and New Way)

Old Way (Pre-iOS 10 or Manual Implementation)

Use a UIRefreshControl manually added to the table view.

Example in UIViewController with UITableView:

swift
override func viewDidLoad() {
    super.viewDidLoad()
    let refreshControl = UIRefreshControl()
    refreshControl.addTarget(self, action: #selector(refreshData), for: .valueChanged)
    tableView.refreshControl = refreshControl
}

@objc func refreshData() {
    // Simulate network fetch
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        self.data = ["Refreshed Item 1", "Refreshed Item 2"]
        self.tableView.reloadData()
        self.tableView.refreshControl?.endRefreshing()
    }
}

Example in UITableViewController:

swift
override func viewDidLoad() {
    super.viewDidLoad()
    refreshControl = UIRefreshControl()
    refreshControl?.addTarget(self, action: #selector(refreshData), for: .valueChanged)
}

@objc func refreshData() {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        self.data = ["Refreshed Item 1", "Refreshed Item 2"]
        self.tableView.reloadData()
        self.refreshControl?.endRefreshing()
    }
}

New Way (iOS 10+ with Diffable Data Source)

Use UIRefreshControl with UITableViewDiffableDataSource for animated updates.

Example in UIViewController with UITableView:

swift
override func viewDidLoad() {
    super.viewDidLoad()
    let refreshControl = UIRefreshControl()
    refreshControl.addTarget(self, action: #selector(refreshData), for: .valueChanged)
    tableView.refreshControl = refreshControl
    configureDataSource()
    applySnapshot(data: data)
}

@objc func refreshData() {
    DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
        self.data = ["Refreshed Item 1", "Refreshed Item 2"]
        self.applySnapshot(data: self.data)
        self.tableView.refreshControl?.endRefreshing()
    }
}

Example in UITableViewController:

swift
class TableViewController: UITableViewController {
    var dataSource: DataSource!
    var data = ["Item 1", "Item 2", "Item 3"]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.identifier)
        refreshControl = UIRefreshControl()
        refreshControl?.addTarget(self, action: #selector(refreshData), for: .valueChanged)
        configureDataSource()
        applySnapshot(data: data)
    }
    
    private func configureDataSource() {
        dataSource = DataSource(tableView: tableView) { tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier, for: indexPath) as! CustomTableViewCell
            cell.configure(title: item, subtitle: "Details")
            return cell
        }
    }
    
    private func applySnapshot(data: [String], animatingDifferences: Bool = true) {
        var snapshot = Snapshot()
        snapshot.appendSections([.main])
        snapshot.appendItems(data)
        dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
    }
    
    @objc func refreshData() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.data = ["Refreshed Item 1", "Refreshed Item 2"]
            self.applySnapshot(data: self.data)
            self.refreshControl?.endRefreshing()
        }
    }
}

Best Practices

  • Reuse Cells: Register and dequeue cells for performance.

  • Accessibility: Set accessibilityLabel and accessibilityTraits for cells.

    swift
    cell.accessibilityLabel = "Item \(indexPath.row)"
    cell.accessibilityTraits = .button
  • Auto Layout: Use constraints (e.g., SnapKit) in custom cells.

  • Performance: Minimize complex cell layouts and use estimatedRowHeight.

  • Testing: Verify selection, editing, swipe actions, and refresh across devices.

  • Diffable Data Source: Prefer UITableViewDiffableDataSource for modern apps.

Troubleshooting

  • Cells Not Displaying: Check dataSource and cellForRowAt implementation.
  • Reuse Issues: Ensure unique reuse identifiers.
  • Animations Failing: Use performBatchUpdates or apply correctly.
  • Refresh Not Working: Verify refreshControl target-action and endRefreshing() call.
  • Accessibility Issues: Test with VoiceOver.
  • Performance Issues: Profile with Instruments for heavy layouts.

Example: Complete UITableView Setup with Diffable Data Source and Pull-to-Refresh

swift
import UIKit
import SnapKit

class CustomTableViewCell: UITableViewCell {
    static let identifier = "CustomCell"
    
    let titleLabel = UILabel()
    let subtitleLabel = UILabel()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupViews() {
        titleLabel.font = .systemFont(ofSize: 16, weight: .bold)
        subtitleLabel.font = .systemFont(ofSize: 14)
        subtitleLabel.textColor = .systemGray
        
        let stackView = UIStackView(arrangedSubviews: [titleLabel, subtitleLabel])
        stackView.axis = .vertical
        stackView.spacing = 4
        contentView.addSubview(stackView)
        
        stackView.snp.makeConstraints { make in
            make.edges.equalTo(contentView).inset(16)
        }
    }
    
    func configure(title: String, subtitle: String) {
        titleLabel.text = title
        subtitleLabel.text = subtitle
        accessibilityLabel = "\(title), \(subtitle)"
    }
}

class HeaderView: UITableViewHeaderFooterView {
    static let identifier = "Header"
    let label = UILabel()
    
    override init(reuseIdentifier: String?) {
        super.init(reuseIdentifier: reuseIdentifier)
        label.font = .systemFont(ofSize: 18, weight: .bold)
        contentView.addSubview(label)
        label.snp.makeConstraints { make in
            make.edges.equalTo(contentView).inset(8)
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func configure(with text: String) {
        label.text = text
    }
}

enum Section: Hashable {
    case featured
    case regular
}

struct Item: Hashable {
    let id = UUID()
    let title: String
}

typealias DataSource = UITableViewDiffableDataSource<Section, Item>
typealias Snapshot = NSDiffableDataSourceSnapshot<Section, Item>

class ViewController: UIViewController, UITableViewDelegate {
    let tableView = UITableView()
    var dataSource: DataSource!
    var featuredItems = [Item(title: "Featured 1"), Item(title: "Featured 2")]
    var regularItems = [Item(title: "Regular 1"), Item(title: "Regular 2")]
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupTableView()
        configureDataSource()
        applySnapshot()
        
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Edit", style: .plain, target: self, action: #selector(toggleEditMode))
    }
    
    private func setupTableView() {
        tableView.register(CustomTableViewCell.self, forCellReuseIdentifier: CustomTableViewCell.identifier)
        tableView.register(HeaderView.self, forHeaderFooterViewReuseIdentifier: HeaderView.identifier)
        tableView.rowHeight = UITableView.automaticDimension
        tableView.estimatedRowHeight = 44
        tableView.delegate = self
        
        let refreshControl = UIRefreshControl()
        refreshControl.addTarget(self, action: #selector(refreshData), for: .valueChanged)
        tableView.refreshControl = refreshControl
        
        view.addSubview(tableView)
        tableView.snp.makeConstraints { make in
            make.edges.equalTo(view.safeAreaLayoutGuide)
        }
    }
    
    private func configureDataSource() {
        dataSource = DataSource(tableView: tableView) { tableView, indexPath, item in
            let cell = tableView.dequeueReusableCell(withIdentifier: CustomTableViewCell.identifier, for: indexPath) as! CustomTableViewCell
            cell.configure(title: item.title, subtitle: "Details")
            return cell
        }
        
        dataSource.supplementaryViewProvider = { tableView, kind, indexPath in
            let header = tableView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: HeaderView.identifier, for: indexPath) as! HeaderView
            header.configure(with: indexPath.section == 0 ? "Featured" : "Regular")
            return header
        }
    }
    
    private func applySnapshot(animatingDifferences: Bool = true) {
        var snapshot = Snapshot()
        snapshot.appendSections([.featured, .regular])
        snapshot.appendItems(featuredItems, toSection: .featured)
        snapshot.appendItems(regularItems, toSection: .regular)
        dataSource.apply(snapshot, animatingDifferences: animatingDifferences)
    }
    
    @objc func refreshData() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.featuredItems = [Item(title: "Refreshed Featured 1"), Item(title: "Refreshed Featured 2")]
            self.regularItems = [Item(title: "Refreshed Regular 1"), Item(title: "Refreshed Regular 2")]
            self.applySnapshot()
            self.tableView.refreshControl?.endRefreshing()
        }
    }
    
    @objc func toggleEditMode() {
        tableView.setEditing(!tableView.isEditing, animated: true)
        navigationItem.rightBarButtonItem?.title = tableView.isEditing ? "Done" : "Edit"
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let item = indexPath.section == 0 ? featuredItems[indexPath.row] : regularItems[indexPath.row]
        print("Selected: \(item.title)")
        tableView.deselectRow(at: indexPath, animated: true)
    }
    
    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        let deleteAction = UIContextualAction(style: .destructive, title: "Delete") { _, _, completion in
            if indexPath.section == 0 {
                self.featuredItems.remove(at: indexPath.row)
            } else {
                self.regularItems.remove(at: indexPath.row)
            }
            self.applySnapshot()
            completion(true)
        }
        return UISwipeActionsConfiguration(actions: [deleteAction])
    }
    
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        return true
    }
    
    func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
        if editingStyle == .delete {
            if indexPath.section == 0 {
                self.featuredItems.remove(at: indexPath.row)
            } else {
                self.regularItems.remove(at: indexPath.row)
            }
            self.applySnapshot()
        }
    }
    
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: HeaderView.identifier) as! HeaderView
        header.configure(with: section == 0 ? "Featured" : "Regular")
        return header
    }
}

// App setup
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        let navController = UINavigationController(rootViewController: ViewController())
        window?.rootViewController = navController
        window?.makeKeyAndVisible()
        return true
    }
}

Resources

Released under the MIT License.