Handling Dynamic Views in Table View Cell

 

In educational applications we often come to a situation when we have to create a user interface for multiple answer type questions. If each question have same number of answers then it can be created very easily by creating a reusable cell in storyboard but what if number of answers in each question varies?

In this post we will see how we can make use of TableView in iOS to create an user interface in which each cell of TableView can have multiple options. This project will be created in XCode 8.1 and Swift 3.0.

We will use following three components to accomplish this:

  • UITableView
  • CheckableTableViewCell: Subclass of UITableViewCell
  • CheckableOptionView: Subclass of UIView

CheckableOptionView:

Lets create a subclass of UIView by File>New>File>Cocoa Touch Class. Give the name CheckableOptionView and make it subclass of UIView. Click Next and Create to create the .swift file. It will create a file with following code:

class CheckableOptionView: UIView {
/*
    // Only override draw() if you perform custom drawing.
    // An empty implementation adversely affects performance during animation.
    override func draw(_ rect: CGRect) {
       // Drawing code
    }
*/
}

Delete the commented code, we are not gonna use it.

Now create following initializers in CheckableOptionView:

init(index: Int) {
    super.init(frame: .zero)
    initializeView(index: index)
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

Now we will implement the initializeView(index:) method. But before implementing this method create a label and a button object in this class and a constant value to use for button tag.

private var checkableButton: UIButton!
private var labelOption: UILabel!
private let buttonTag: Int = 8080

Let’s create initializeView method.

private func initializeView(index: Int) {
}

Create UIButton object and set tag with given index.

checkableButton = UIButton(type: .custom)
checkableButton.tag = buttonTag + index

Now set the button image for different states

checkableButton.setImage(UIImage(named: “unchecked”), for: .normal)
checkableButton.setImage(UIImage(named: “checked”), for: .selected)

Add action target to button

checkableButton.addTarget(self, action: #selector(optionSelected(sender:)), for: .touchUpInside)

Now create the option label which can have multiple lines

labelOption = UILabel()
labelOption.numberOfLines = 0
labelOption.lineBreakMode = .byWordWrapping

Add Label and Button to the view:

addSubview(checkableButton)
addSubview(labelOption)

Now disable converting auto resizing masks to constraint for each view:

translatesAutoresizingMaskIntoConstraints = false
checkableButton.translatesAutoresizingMaskIntoConstraints = false
labelOption.translatesAutoresizingMaskIntoConstraints = false

Add following constraints to Button:

//Height = 30
checkableButton.heightAnchor.constraint(equalToConstant: 30).isActive = true

//Width = 30
checkableButton.widthAnchor.constraint(equalToConstant: 30).isActive = true

//Leading = 8
checkableButton.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8).isActive = true

//Vertically center
centerYAnchor.constraint(equalTo: checkableButton.centerYAnchor).isActive = true

Add following constraints to Label:

//Lable leading = button trailing + 8
labelOption.leadingAnchor.constraint(equalTo: checkableButton.trailingAnchor, constant: 8).isActive = true

//Top = 8
labelOption.topAnchor.constraint(equalTo: topAnchor, constant: 8).isActive = true

//Trailing = 8
trailingAnchor.constraint(equalTo: labelOption.trailingAnchor, constant: 8).isActive = true

//Bottom = 8
bottomAnchor.constraint(equalTo: labelOption.bottomAnchor, constant: 8).isActive = true

Now create a protocol for a delegate call to cell as follows and also create a weak property to store the delegate.

protocol CheckableOptionViewDelegate: class {
    func checkableOptionView(option: Int)
}

public weak var delegate: CheckableOptionViewDelegate?

Now create following properties for multiple operations:

//To mark an option selected
public var selected: Bool = false {
    didSet {
       setSelected(selected: selected)
    }
}

//To set the option text
public var optionText: String = "" {
    didSet {
       setOptionText(text: optionText)
    }
}

//To get the index of the option
public var option: Int {
    get {
        return checkableButton.tag - buttonTag
    }
}

//To set whether option can be selected or not
public var optionSelectable: Bool = true {
    didSet {
       if !optionSelectable {
          checkableButton.isSelected = false
       }
    }
}

Now create the method setSelected and setOptionText

public func setOptionText(text: String) {
    labelOption.text = text
}

public func setSelected(selected: Bool) {
    if !optionSelectable {
       return
    }

    if selected {
       checkableButton.isSelected = true
    } else {
       checkableButton.isSelected = false
    }
}

Let’s create the action method of option button

@objc private func optionSelected(sender: UIButton) {

    if !optionSelectable {
       return
    }

    if !sender.isSelected {
       sender.isSelected = true

       //Call delegate method
       delegate?.checkableOptionView(option: sender.tag - buttonTag)
    }
}

That’s it. Our CheckableOptionView class is complete.

CheckableTableViewCell:

Lets create a subclass of UITableViewCell by File>New>File>Cocoa Touch Class. Give the name CheckableTableViewCell and make it subclass of UITableViewCell. Click Next and Create to create the .swift file.

Change the initial code so that it should have following code:

class CheckableTableViewCell: UITableViewCell {

    override func awakeFromNib() {
       super.awakeFromNib()
       initializeCell()
    }

}

Now, let’s create following private variables to create a header and footer for question and answer for a cell:

private let headerView = UIView()
private let footerView = UIView()
private let headerLabel = UILabel()
private let footerLabel = UILabel()

Here, header label and view will be used for displaying question and footer label and view will be used to display answer.

Now, it’s time to define the initializeCell method. In this method we will initialize header and footer view and add constraints to them. Initially the footer view will be invisible to user to hide the answer.

Create following method:

private func initializeCell() {
}

Make header label and footer label number of lines to zero and line break mode to word wrapping so that we can display multiple lines in it. Finally add them to cell’s contentView as subview.

headerLabel.numberOfLines = 0
headerLabel.lineBreakMode = .byWordWrapping

footerLabel.numberOfLines = 0
footerLabel.lineBreakMode = .byWordWrapping

headerView.addSubview(headerLabel)
footerView.addSubview(footerLabel)

contentView.addSubview(headerView)
contentView.addSubview(footerView)

Set background of footer view (Answer) to green. (Or any color you like).

footerView.backgroundColor = UIColor.green

Now disable the auto resizing masks to convert into constraint for each view by following code:

headerView.translatesAutoresizingMaskIntoConstraints = false
footerView.translatesAutoresizingMaskIntoConstraints = false
headerLabel.translatesAutoresizingMaskIntoConstraints = false
footerLabel.translatesAutoresizingMaskIntoConstraints = false

Let’s add constraint to Question View(Header).

//headerLabel.top = headerView.top + 8
headerLabel.topAnchor.constraint(equalTo: headerView.topAnchor, constant: 8).isActive = true

//headerLabel.leading = headerView.leading + 8
headerLabel.leadingAnchor.constraint(equalTo: headerView.leadingAnchor, constant: 8).isActive = true

//headerView.trailing = headerLabel.trailing + 8
headerView.trailingAnchor.constraint(equalTo: headerLabel.trailingAnchor, constant: 8).isActive = true

//headerView.bottom = headerLabel.bottom + 8
headerView.bottomAnchor.constraint(equalTo: headerLabel.bottomAnchor, constant: 8).isActive = true

//headerView.leading = contentView.leading + 8
headerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8).isActive = true

//headerView.top = contentView.top + 8
headerView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 8).isActive = true

//contentView.trailing = headerView.trailing + 8
contentView.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: 8).isActive = true

Add following constraints to Answer View(Footer).

//footerLabel.leading = footerView.leading + 8
footerLabel.leadingAnchor.constraint(equalTo: footerView.leadingAnchor, constant: 8).isActive = true

//footerLabel.top = footerView.top + 8
footerLabel.topAnchor.constraint(equalTo: footerView.topAnchor, constant: 8).isActive = true

//footerVoew.trailing = footerLabel.trailing + 8
footerView.trailingAnchor.constraint(equalTo: footerLabel.trailingAnchor, constant: 8).isActive = true

//footerView.bottom = footerLabel.bootom + 8
footerView.bottomAnchor.constraint(equalTo: footerLabel.bottomAnchor, constant: 8).isActive = true

//footerView.leading = contentView.leading + 8
footerView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8).isActive = true

//contentView.bottom = footerView.bottom + 8
contentView.bottomAnchor.constraint(equalTo: footerView.bottomAnchor, constant: 8).isActive = true

//contentView.trailing = footerView.trailing + 8
contentView.trailingAnchor.constraint(equalTo: footerView.trailingAnchor, constant: 8).isActive = true

Cell initialization is done here. Let’s create some properties which can be used for different operations on the cell.(Methods used in these properties will be defined later in the post).

//Set the options in a question.
public var options: Array<String> = [] {
    didSet {
       addOptions(options: options)
    }
}

//Set the selected option index.
public var selectedOption: Int = -1 {
    didSet {
       setSelectedOption(index: selectedOption)
    }
}

//Set the question string of the cell.
public var question: String = "" {
    didSet {
       headerLabel.text = question
    }
}

//Set the answer string for that question.
public var answer: String = "" {
    didSet {
       footerLabel.text = answer
    }
}

//Set whether the options are selectable or not.
public var optionSelectable: Bool = true {
    didSet {
       setOption(selectable: optionSelectable)
    }
}

Now, let’s create the method setSelectedOption(index: selectedOption).

public func setSelectedOption(index: Int) {
    //If options are not selectable do nothing.
    if !optionSelectable {
       return
    }

    // Find all subviews
    for subview in contentView.subviews {
       //If it is CheckableOptionView
       if let checkableOptionView = subview as? CheckableOptionView {
          //If the option index is equal to given index mark it as selected otherwise mark it as deselected.
          if checkableOptionView.option == index {
             checkableOptionView.selected = true
          } else {
             checkableOptionView.selected = false
          }
       }
    }
}

Let’s create a method to clear the selection for entire row. In this method we will find all the subviews of the content view of cell(Line 2). If the subview is type of CheckableOptionView(Line 3) then we will deselect that option.

public func clearSelection() {
    for subview in contentView.subviews {
       if let checkableOptionView = subview as? CheckableOptionView {
          checkableOptionView.selected = false
       }
    }
}

This method will be used to mark the CheckableOptionView that whether it is selectable or not.

public func setOption(selectable: Bool) {
    //Find each subview and check if it is of type CheckableOptionView, set the selectability of that view.
    for subview in contentView.subviews {
       if let checkableOptionView = subview as? CheckableOptionView {
          checkableOptionView.optionSelectable = selectable
       }
    }
}

Let’s create a method to change the background color of any option. It will have two parameters color and the index of the option which background color has to be change. In this method we will find all the subviews of cell’s content view and check if it is of type CheckableOptionView(Line 2 and 3) then we check that the option index of that view is equal to the given index(Line 4). If it is true then we will set the background color of that view to the given color in method parameter.

public func setBackground(color: UIColor, forOption option: Int) {
    for subview in contentView.subviews {
       if let checkableOptionView = subview as? CheckableOptionView {
          if checkableOptionView.option == option {
             checkableOptionView.backgroundColor = color
          }
       }
    }
}

Following method can be used to set the Question Text.

public func setQuestion(question: String) {
    headerLabel.text = question
}

Following method can be used to set the Answer Text.

public func setAnswer(answer: String) {
    footerLabel.text = answer
}

Let’s create a method to show the answer of the question. In this method we will find all the constraints in the footerView(Line 2) and check if it is a height constraint(Line 3). If the constraint is of height then we will disable that constraint(Line 4).

public func showAnswer() {
    for constraint in footerView.constraints {
       if constraint.firstAnchor == footerView.heightAnchor {
          constraint.isActive = false
       }
    }
}

Now, let’s create the method addOptions(options: [String]) to add the options of question for the cell, it will take an array of strings:

public func addOptions(options: [String]) {

    // Store previous option view to add constraints
    var previousView: CheckableOptionView? = nil

    var i=0
    var isFirst = true

    // For all options given in the array
    while i < options.count {

       // If view is already exist (Due to reusability of  cell)
       if let optionView = contentView.subviews[safe: i+2] as? CheckableOptionView {

          //Reset the view with given parameters
          optionView.optionText = options[i]
          optionView.optionSelectable = optionSelectable
          optionView.selected = false
          optionView.backgroundColor = UIColor.white
          previousView = optionView
       } else { // If view is not available create a new view and add constraints.
          let optionView = CheckableOptionView(index: i)
          optionView.optionText = options[i]
          optionView.delegate = self
          optionView.optionSelectable = optionSelectable
          contentView.addSubview(optionView)
          optionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8).isActive = true
          if let previous = previousView {
             if isFirst { // Remove the bottom constraint of previous view with footer view
                for constraint in contentView.constraints {
                   if constraint.firstAnchor == footerView.topAnchor {
                      constraint.isActive = false
                   }
                }
             }

             // Add top constraint to bottom of previous view
             optionView.topAnchor.constraint(equalTo: previous.bottomAnchor).isActive = true
          } else {

             //If previous view is not available then add top constraint to header view’s bottom.
             optionView.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 8).isActive = true
          }

          // Add leading and trailing constraint to content view.
          contentView.trailingAnchor.constraint(equalTo: optionView.trailingAnchor, constant: 8).isActive = true
          optionView.heightAnchor.constraint(greaterThanOrEqualToConstant: 30).isActive = true

          isFirst = false
          previousView = optionView
       }
       i = i + 1
    }

    if let previous = previousView {

       // Add last view’s bottom to top of the footer view.
       footerView.topAnchor.constraint(equalTo: previous.bottomAnchor, constant: 8).isActive = true
    }

    // Initially hide the footer view (Answer View)
    footerView.heightAnchor.constraint(equalToConstant: 0).isActive = true

    // If there are more view’s than the given options remove them. (Due to reusability of cell).
    while i < contentView.subviews.count - 2 {
       if let optionView = contentView.subviews[safe: i+2] as? CheckableOptionView {
          optionView.removeFromSuperview()
       }
    }
}

Now, let’s create a protocol for delegate call to view controller:

protocol CheckableTableViewCellDelegate: class {
    func checkableTableViewCell(_ cell: CheckableTableViewCell, option: Int)
}

Let’s implement the CheckableOptionViewDelegate as follows:

extension CheckableTableViewCell: CheckableOptionViewDelegate {
    func checkableOptionView(option: Int) {

       //Deselect all other options
       for subview in contentView.subviews {
          if let checkableOptionView = subview as? CheckableOptionView {
             if checkableOptionView.option != option {
                checkableOptionView.selected = false
             }
          }
       }

       // Call delegate method to inform view controller.
       delegate?.checkableTableViewCell(self, option: option)
    }
}

Add following extension to safely access the array elements:

extension Collection where Indices.Iterator.Element == Index {

    // Return object if present otherwise nil.
    subscript (safe index: Index) -> Generator.Element? {
       return indices.contains(index) ? self[index] : nil
    }
}

Table View Implementation:

To use the given CheckableTableViewCell, add a UITableView from storyboard and set the class of the reusable cell to CheckableTableViewCell as given screenshot:

Screen4

Now, add the following code in the viewDidLoad of your view controller:

tableView.estimatedRowHeight = 44 //Or any value you want.
tableView.rowHeight = UITableViewAutomaticDimension
tableView.dataSource = self
tableView.delegate = self

In cellForRowAt indexPath method use following code:

//Dequeue the cell. Replace `cell` with your reusable identifier
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! CheckableTableViewCell

cell.delegate = self

cell.question = “Question Text”
cell.options = [“Option1”, “Option2”, “Option3”] // Option array
cell.answer = "Answer Text"

If you want to mark an option selected use following code:

cell.selectedOption = 1 // Starts with index 0.

To show the answer of the question use following code:

cell.showAnswer()

To change the background color of any option use following code:

cell.setBackground(color: UIColor.red, forOption: 1)

That’s it. You are ready to go. Compile and run the project.

Leave a Reply