To avoid confusion, this article focuses on expanding and contracing the height of a single tableview cell when it is tapped. I googled around and establish out many people used the same terms for inserting new cells nether the tapped cell, which is not what this article is most, I will write on this topic in the hereafter.

At the cease of this tutorial, y'all volition be able to implement this :

expanding gif

This tutorial assumes yous know how to implement tableview prison cell with dynamic peak (Automatic Dimension), as in each cell can contain different content height, eg: lengths of label text, different image sizes.

Say we have a table view cell with prototype and label fix like below :

expandingCell

You can download the starter project here : Expanding cell starter projection

The image view (avatarImageView) and characterization (messageLabel) are initialized in the cellForRowAt function :

          // ViewController.swift  extension VariableViewController : UITableViewDataSource {     func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {         render three     }          func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {              // if cant dequeue prison cell, return a default cell         guard let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as? VariableTableViewCell else {             print("failed to get cell")             render UITableViewCell()         }                  cell.avatarImageView.prototype = UIImage(named: avatars[indexPath.row])         jail cell.messageLabel.text = letters[indexPath.row]         cell.messageLabel.numberOfLines = 0                return cell     } }                  

Earlier nosotros implement the expanding / contracting animation, we would demand to declare an IndexSet to keep track of which prison cell accept been expanded, by storing the row index into the IndexSet, so that when we tap on the expanded cell, it would contract instead of expanding.

Employ IndexSet to proceed track of which cell has been expanded

From Apple documentation on IndexSet,

A collection of unique integer values that represent the indexes of elements in another collection.

You tin remember of IndexSet like an array, information technology can concord multiple integers, but non of the integers are repeating. This way, we won't need to worry nearly adding repeating row index into the array.

indexset

"Why tin can't we merely utilise an array of boolean to track which cell has been expanded?" , I heard you, this solution works too, say you lot tin can cheque if a cell has been expanded  like this :

          // 10 cells var expandedCells : [Boolean] = Array(repeating: false, count: 10)  if(expandedCells[index] == true) {   // ... show the cell expanded }                  

This works well if you have a fixed amount of cells in the tabular array view, but if yous take dynamic data that will be added from external API, say like Facebook timeline which keep adding more statuses as you curl, y'all will need to adapt the size of the expandedCells array as more than cells are existence added, this process can easily exist error prone!

By using IndexSet, nosotros won't need to worry virtually the size for the dataSource, we merely need to add or remove index of the jail cell that is expanded.

Nosotros can define an IndexSet to keep rail of the index of cell expanded in the view controller like this :

          class ViewController: UIViewController {          @IBOutlet weak var tableView: UITableView!          // this will contain the index of the row (integer) that is expanded     var expandedIndexSet : IndexSet = []        // ... }                  

Animative the cell to expand and contract

Now that we accept an IndexSet to continue track of which cell has been expanded, we can layout the cell expanded / contracted state in cellForRowAt like this :

          extension VariableViewController : UITableViewDataSource {   // ...    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {     guard let cell = tableView.dequeueReusableCell(withIdentifier: "jail cell", for: indexPath) as? VariableTableViewCell else {       impress("failed to get cell")       return UITableViewCell()     }      prison cell.avatarImageView.prototype = UIImage(named: avatars[indexPath.row])     jail cell.messageLabel.text = letters[indexPath.row]      // if the cell is expanded     if expandedIndexSet.contains(indexPath.row) {       // the label tin accept as many lines it demand to brandish all text       cell.messageLabel.numberOfLines = 0     } else {       // if the prison cell is contracted       // only show first three lines       jail cell.messageLabel.numberOfLines = three     }      render cell   } }                  

Equally we want the cell to exist expanded / contracted on tap, we then implement the didSelectAtRow delegate function :

          extension ViewController : UITableViewDelegate {          func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {         tableView.deselectRow(at: indexPath, blithe: true)                  // if the cell is already expanded, remove it from the indexset to contract it         if(expandedIndexSet.contains(indexPath.row)){             expandedIndexSet.remove(indexPath.row)         } else {             // if the cell is non expanded, add together it to the indexset to aggrandize it             expandedIndexSet.insert(indexPath.row)         }                  // the animation magic happens here         // this will call cellForRow for the indexPath you supplied, and animate the changes         tableView.reloadRows(at: [indexPath], with: .automatic)     } }                  

The central for the animation is calling tableView.reloadRows(at:, with:) . You supply an array of rows (indexpath) that needs to exist reloaded, so the tableview volition reload them past calling cellForRowAt: on these cell to layout them again, and animate the transition with the RowAnimation you passed into the with: parameter.

cellForRowAt

Hither's a list of available RowAnimation we tin can use to breathing the cell row changes, but mostly we will utilise the .automatic or .fade event as it looks better.

Implementing the cell blitheness was easier than I originally thought! Thanks reloadRows for the heavy lifting.

If you want to cheque if an UILabel is truncated (with "..." at the end of text because of clipping), I accept written a short extension here, this might be useful if you are checking whether to show a "read more" button on the tableview cell depending if the label is beingness truncated. eg. If the the label text is short and didn't get truncated, don't prove the "read more" button.

Download the sample expanding cell projection

Not certain if you are doing it right on the expanding cell?

Download the sample project and compare it yourself!

https://github.com/fluffyes/expandingCell

demo