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:
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.