I had to cobble this together out of a bunch of StackOverflow answers so…

I have a UITableView where I want to change the appearance of a cell when it’s selected. I want to change both the height of the cell, and the UITableViewCell class used.

It turned out that I had to do all of these…

var selectedIndexPath: IndexPath?

override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
    if selectedIndexPath != indexPath {
        // don't do this if this was already the selected row
        selectedIndexPath = indexPath
        tableView.reloadRows(at: [indexPath], with: .none)  // animation will be handled later
    }
    return indexPath
}

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    // force the tableView to reset the height of the selected cell and animate it
    tableView.beginUpdates()
    tableView.endUpdates()
}

override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
    if selectedIndexPath == indexPath {
        selectedIndexPath = nil
    }
    // reloading the row will change both height and class, and also clears the selection
    tableView.reloadRows(at: [indexPath], with: .none)
}

override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    if indexPath == selectedIndexPath {
        return SELECTED_HEIGHT
    }
    return NORMAL_HEIGHT
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    if indexPath == tableView.indexPathForSelectedRow || indexPath == selectedIndexPath {
        return tableView.dequeueReusableCell(withIdentifier: "SelectedCell", for: indexPath) as? SelectedTableViewCell
    }
    return tableView.dequeueReusableCell(withIdentifier: "NormalCell", for: indexPath) as? NormalTableViewCell
}