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! 🙌
Contents
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 questions, please feel free to leave a comment below