How to make Expandable TableView using Swift

Last updated on: September 30, 2020

In this tutorial, I’m going to show you how to make an expandable TableView with a nice animation.

Also, we’re not going to use any third-party library to do it! 🙌

Preparing the Data and Model

Before we start, create an empty swift file, name it Data and paste the following code inside:

import Foundation

class Data {
    var items = [
        DataModel(
            question: "Item 0",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
        ),
        DataModel(
            question: "Item 1",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam"
        ),
        DataModel(
            question: "Item 2",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut"
        ),
        DataModel(
            question: "Item 3",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."
        ),
        DataModel(
            question: "Item 4",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
        ),
        DataModel(
            question: "Item 5",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
        ),
        DataModel(
            question: "Item 6",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident"
        ),
        DataModel(
            question: "Item 7",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur."
        ),
        DataModel(
            question: "Item 8",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
        ),
        DataModel(
            question: "Item 9",
            answer: "Lorem ipsum dolor sit amet"
        ),
        DataModel(
            question: "Item 10",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
        ),
        DataModel(
            question: "Item 11",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit"
        ),
        DataModel(
            question: "Item 12",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor"
        ),
        DataModel(
            question: "Item 13",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
        ),
        DataModel(
            question: "Item 14",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore"
        ),
        DataModel(
            question: "Item 15",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor"
        ),
        DataModel(
            question: "Item 16",
            answer: "Lorem ipsum"
        ),
        DataModel(
            question: "Item 17",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in"
        ),
        DataModel(
            question: "Item 18",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat"
        ),
        DataModel(
            question: "Item 19",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
        ),
        DataModel(
            question: "Item 20",
            answer: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
        )
    ]
}
Code language: Swift (swift)

As you see the model (DataModel), contains:

  • Question
  • Answer

Create another empty swift file, name it DataModel and paste:

import Foundation

class DataModel {
    var question: String?
    var answer: String?
    
    init(question: String, answer: String) {
        self.question = question
        self.answer = answer
    }
}Code language: Swift (swift)

Creating TableView and Cell

In your ViewController, add a UITableView, and create an IBOutlet in your swift file:

Create the Cell by going to File > New > File and select Cocoa Touch Class

Next, name it TableViewCell and select UITableViewCell as a Subclass of, and check the box Also create XIB file

In the .xib file, add two UILabels, one for the question and one for the answer, with height 44 each, and create the IBOutlets in your swift file (Including the height constraint of the answer UILabel).

If you noticed, in this example, I’m using the PaddingLabel instead of UILabel.

You can find how to use the PaddingLabel (UILabel with padding) in the tutorial recently I wrote, here!

Select the cell, go to the Attributes Inspector, and set ‘tableviewcellid‘ as Identifier

In the question UILabel, set the height to be Greater Than or Equal

And in the answer UILabel, set the top constraint Priority to 999

Now, go back to TableView and add the Delegate and DataSource protocol, get the data, and register the cell we just created.

class TableViewController: UIViewController {
   
    @IBOutlet var tableView: UITableView!
    var itemsArray = [DataModel]()
 
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.delegate = self
        tableView.dataSource = self
        getData()
        
        tableView.estimatedRowHeight = 60
        tableView.rowHeight = UITableView.automaticDimension
        
        let nib = UINib(nibName: "TableViewCell", bundle: nil)
        tableView.register(nib, forCellReuseIdentifier: "tableviewcellid")
        tableView.separatorStyle = .none
    }
    
    func getData() {
        itemsArray = Data().items
    }
    
    // Reload the tableview data when you rotate the device
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.reloadData()
    }
 
}

extension TableViewController: UITableViewDelegate, UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return itemsArray.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let cell = tableView.dequeueReusableCell(withIdentifier: "tableviewcellid", for: indexPath) as? TableViewCell else { return UITableViewCell() }

        // ...

        cell.selectionStyle = .none
        return cell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let cell = tableView.cellForRow(at: indexPath) as? TableViewCell else { return }
        cell.answerLabel.text = self.itemsArray[indexPath.row]
    }
}Code language: Swift (swift)

Making the Cell Expandable

To make the cell expandable, we have to calculate the height of the String that shows the answer, by using a String extension.

Add this add at the bottom of your file (outside the class), of the ViewController that has the TableView:

extension String {
    func height(width: CGFloat, font: UIFont) -> CGFloat {
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: .greatestFiniteMagnitude))
        label.numberOfLines = 0
        label.text = self
        label.font = font
        label.sizeToFit()
        return label.frame.height
    }
}Code language: Swift (swift)

This will give us how much height do we need to expand the cell to show the answer.

Also, we need to create an Array to store all the states of the cells. In this example, we call it isExpanded

class TableViewController: UIViewController {

    // ...
   
    var isExpanded = [Bool]()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // ...
        getData()
        setCellState()
        
        // ...
    }
    
    func getData() {
        itemsArray = Data().items
    }
    
    // Set the cell state to false, because all cells are collapsed at the beginning
    func setCellState() {
        for _ in 0..<itemsArray.count {
            isExpanded.append(false)
        }
    }

    // ...

}

extension TableViewController: UITableViewDelegate, UITableViewDataSource {
    
    // ...

}

extension String {
    func height(width: CGFloat, font: UIFont) -> CGFloat {
        let label = UILabel(frame: CGRect(x: 0, y: 0, width: width, height: .greatestFiniteMagnitude))
        label.numberOfLines = 0
        label.text = self
        label.font = font
        label.sizeToFit()
        return label.frame.height
    }
}Code language: Swift (swift)

Let’s go back to the TableViewCell’s swift file, and add the method set that will be called every time a cell is created.

class TableViewCell: UITableViewCell {
    
    @IBOutlet var questionLabel: PaddingLabel!
    @IBOutlet var answerLabel: PaddingLabel!
    @IBOutlet var answerLabelHeight: NSLayoutConstraint!
    
    override func awakeFromNib() {
        super.awakeFromNib()
       
        // ...

    }
    
    func set(content: DataModel, state: Bool) {
        self.questionLabel.text = content.question
        guard let stringHeight = content.answer?.height(width: self.answerLabel.frame.width - (self.answerLabel.paddingLeft + self.answerLabel.paddingRight), font: .systemFont(ofSize: 17, weight: .regular)) else { return }
        if state == true {
            self.answerLabel.text = content.answer
            // Answer Label Padding
            answerLabel.paddingTop = 8
            answerLabel.paddingBottom = 8
            answerLabelHeight.constant = stringHeight + answerLabel.paddingTop + answerLabel.paddingBottom
        } else {
            self.answerLabel.text = ""
            // Answer Label Padding
            answerLabel.paddingTop = 0
            answerLabel.paddingBottom = 0
            answerLabelHeight.constant = 0
        }
        layoutIfNeeded()
    }
}Code language: Swift (swift)

Now, return to the TableView, and call it from the cellForRowAt method

// ...
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    guard let cell = tableView.dequeueReusableCell(withIdentifier: "tableviewcellid", for: indexPath) as? TableViewCell else { return UITableViewCell() }
    
    cell.set(content: self.itemsArray[indexPath.row], state: isExpanded[indexPath.row])
    
    // ...
    return cell
}
// ...Code language: Swift (swift)

Next, every time we tap on a cell, the didSelectRowAt method gets called and expand/collapse the cell accordingly.

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    guard let cell = tableView.cellForRow(at: indexPath) as? TableViewCell else { return }
    
    cell.answerLabel.text = self.itemsArray[indexPath.row].answer
    if isExpanded[indexPath.row] == true {
        
        // Remove Padding
        cell.answerLabel.paddingTop = 0
        cell.answerLabel.paddingBottom = 0
        
        // Hide the Answer Label
        cell.answerLabelHeight.constant = 0
        
        // The cell is collapsed
        isExpanded[indexPath.row] = false
    } else {
        
        // Add Padding
        cell.answerLabel.paddingTop = 8
        cell.answerLabel.paddingBottom = 8
        
        // Get the height of the Answer Label by calculating the string
        guard let stringHeight = self.itemsArray[indexPath.row].answer?.height(width: cell.answerLabel.frame.width - (cell.answerLabel.paddingLeft + cell.answerLabel.paddingRight), font: .systemFont(ofSize: 17, weight: .regular)) else { return }
        // Expand Answer Label
        cell.answerLabelHeight.constant = stringHeight + cell.answerLabel.paddingTop + cell.answerLabel.paddingBottom
        
        // The cell is expanded
        isExpanded[indexPath.row] = true
    }
    // Expand/Collapse the cell with animation (The smaller the number, the faster the cell will expand/collapse)
    UIView.animate(withDuration: 0.3) {
        tableView.beginUpdates()
        cell.layoutIfNeeded()
        tableView.endUpdates()
    }
}
Code language: Swift (swift)
You can find the final project here

If you have any questionsplease feel free to leave a comment below

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments