At this point in the quiz, the app is supposed to calculate the final score. At the moment, it crashes after selecting an answer on the final question, with the title error appearing on the third line:
var score = 0
for question in QuestionController.questionsList {
score += question.selectedAnswerIndex!
Full code for that file below. It works fine in previous iterations of the app, but just started today. I know there's an issue with force-unwrapping optionals, but unclear why it only occurs in this instance:
import UIKit
class ViewController: UIViewController {
var window: UIWindow?
override func viewDidLoad() {
super.viewDidLoad()
self.title="Quiz"
self.view.backgroundColor=UIColor.white
setupViews()
}
@objc func btnGetStartedAction() {
let v=QuestionController()
self.navigationController?.pushViewController(v, animated: true)
}
func setupViews() {
self.view.addSubview(lblTitle)
lblTitle.topAnchor.constraint(equalTo: self.view.topAnchor, constant: 150).isActive=true
lblTitle.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive=true
lblTitle.widthAnchor.constraint(equalToConstant: 250).isActive=true
lblTitle.heightAnchor.constraint(equalToConstant: 250).isActive=true
self.view.addSubview(btnGetStarted)
btnGetStarted.topAnchor.constraint(equalTo: lblTitle.bottomAnchor, constant: 20).isActive=true
btnGetStarted.heightAnchor.constraint(equalToConstant: 50).isActive=true
btnGetStarted.widthAnchor.constraint(equalToConstant: 150).isActive=true
btnGetStarted.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive=true
btnGetStarted.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: 0).isActive=true
}
let lblTitle: UILabel = {
let lbl=UILabel()
lbl.text="Have you ever wondered which character you are on Friends? Answer the question on this quiz."
lbl.textColor=UIColor.black
lbl.textAlignment = .center
lbl.font = UIFont.systemFont(ofSize: 30)
lbl.adjustsFontSizeToFitWidth = true
lbl.numberOfLines=0
lbl.sizeToFit()
lbl.translatesAutoresizingMaskIntoConstraints=false
return lbl
}()
let btnGetStarted: UIButton = {
let btn=UIButton()
btn.setTitle("Get Started", for: .normal)
btn.setTitleColor(UIColor.white, for: .normal)
btn.backgroundColor=UIColor.blue
btn.layer.cornerRadius=5
btn.layer.masksToBounds=true
btn.translatesAutoresizingMaskIntoConstraints=false
btn.addTarget(self, action: #selector(btnGetStartedAction), for: .touchUpInside)
return btn
}()
}
struct Question {
var questionString: String?
var answers: [String]?
var selectedAnswerIndex: Int?
}
class QuestionController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let cellId = "cellId"
let headerId = "headerId"
var tableView: UITableView?
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.tableView?.frame = CGRect(x: 0, y: 64, width: self.view.frame.width, height: self.view.frame.height - 64)
}
static var questionsList: [Question] = [Question(questionString: "What is your favorite type of food?", answers: ["Sandwiches", "Pizza", "Seafood", "Unagi"], selectedAnswerIndex: nil), Question(questionString: "What do you do for a living?", answers: ["Paleontologist", "Actor", "Chef", "Waitress"], selectedAnswerIndex: nil), Question(questionString: "Were you on a break?", answers: ["Yes", "No"], selectedAnswerIndex: nil)]
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.title = "Question"
navigationController?.navigationBar.tintColor = UIColor.white
navigationItem.backBarButtonItem = UIBarButtonItem(title: "Back", style: .plain, target: nil, action: nil)
tableView = UITableView()
tableView?.dataSource = self
tableView?.delegate = self
tableView?.estimatedRowHeight = 140
tableView?.sectionHeaderHeight = 100
self.tableView?.rowHeight = UITableViewAutomaticDimension
self.view.addSubview(self.tableView!)
tableView?.register(AnswerCell.self, forCellReuseIdentifier: cellId)
tableView?.register(QuestionHeader.self, forHeaderFooterViewReuseIdentifier: headerId)
tableView?.tableFooterView = UIView()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if let index = navigationController?.viewControllers.index(of: self) {
let question = QuestionController.questionsList[index]
if let count = question.answers?.count {
return count
}
}
return 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath as IndexPath) as! AnswerCell
if let index = navigationController?.viewControllers.index(of: self) {
let question = QuestionController.questionsList[index]
cell.nameLabel.text = question.answers?[indexPath.row]
cell.nameLabel.numberOfLines = 0
cell.nameLabel.lineBreakMode = .byWordWrapping
}
return cell
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerId) as! QuestionHeader
if let index = navigationController?.viewControllers.index(of: self) {
let question = QuestionController.questionsList[index]
header.nameLabel.text = question.questionString
header.nameLabel.numberOfLines = 0
header.nameLabel.lineBreakMode = .byWordWrapping
}
return header
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let index = navigationController?.viewControllers.index(of: self) {
QuestionController.questionsList[index].selectedAnswerIndex = indexPath.item
if index < QuestionController.questionsList.count - 1 {
let questionController = QuestionController()
navigationController?.pushViewController(questionController, animated: true)
} else {
let controller = ResultsController()
navigationController?.pushViewController(controller, animated: true)
}
}
}
}
class ResultsController: UIViewController {
let resultsLabel: UILabel = {
let label = UILabel()
label.text = "Congratulations! You'd make a great Ross!"
label.contentMode = .scaleToFill
label.numberOfLines = 0
label.translatesAutoresizingMaskIntoConstraints = false
label.textAlignment = .center
label.font = UIFont.boldSystemFont(ofSize: 30)
return label
}()
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Done", style: .plain, target: self, action: #selector(done(sender:)))
navigationItem.title = "Results"
view.backgroundColor = UIColor.white
view.addSubview(resultsLabel)
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|[v0]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["v0": resultsLabel]))
view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|[v0]|", options: NSLayoutFormatOptions(), metrics: nil, views: ["v0": resultsLabel]))
let names = ["Ross", "Joey", "Chandler", "Monica", "Rachel", "Phoebe"]
var score = 0
for question in QuestionController.questionsList {
score += question.selectedAnswerIndex!
}
let result = names[score % names.count]
resultsLabel.text = "Congratulations! \(result)."
}
@objc func done(sender: UIBarButtonItem) {
navigationController?.popToRootViewController(animated: true)
}
}
The problem is caused by your use of the view controller's position in the navigation controller's viewControllers
array to determine the question index. The "get started" view controller is at position 0, and your first "question" view controller is at position 1.
This means that you never ask question '0' about the user's food preferences.
Then when you iterate through the questions and force unwrap selectedAnswerIndex
you get a crash.
In general you should never force unwrap unless:
nil
A second point is don't try to be too clever. You could pass a simple int
of the current question index to your view controller. While this isn't as "clever" as using the viewControllers
array to determine the question index, it is much easier to see what is going on and won't break if the number of view controllers in the stack changes.
class QuestionController: UIViewController, UITableViewDelegate, UITableViewDataSource {
let cellId = "cellId"
let headerId = "headerId"
var tableView: UITableView?
var questionIndex = 0
....
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return QuestionController.questionsList[index].answers?.count ?? 0
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath as IndexPath) as! AnswerCell
let question = QuestionController.questionsList[questionIndex]
cell.nameLabel.text = question.answers?[indexPath.row]
cell.nameLabel.numberOfLines = 0
cell.nameLabel.lineBreakMode = .byWordWrapping
return cell
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerId) as! QuestionHeader
let question = QuestionController.questionsList[questionIndex]
header.nameLabel.text = question.questionString
header.nameLabel.numberOfLines = 0
header.nameLabel.lineBreakMode = .byWordWrapping
return header
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
QuestionController.questionsList[questionIndex].selectedAnswerIndex = indexPath.item
if questionIndex < QuestionController.questionsList.count - 1 {
let questionController = QuestionController()
questionController.questionIndex = questionIndex+1
navigationController?.pushViewController(questionController, animated: true)
} else {
let controller = ResultsController()
navigationController?.pushViewController(controller, animated: true)
}
}
}
Not only does this code work, it is easier to see what is going on and it is fewer lines of code because you don't need to keep unwrapping the optional index from the view controllers array; for example, numberOfRowsInSection
goes from 7 lines to 1.
Also, in this case I can't see a good reason for the questionString
and answers
properties of the Question
struct to be optional; A question must have a question and a set of answers. I would probably also change questionString
to just question
- putting the type in the property name is redundant.
The tableView
property of your QuestionController
is a good place to use an implicitly unwrapped optional - You know there is going to be tableview because one of the first things your code does is create it. Using an implicitly unwrapped optional (UITableView!
instead of UITableView?
) will save you from having to continually unwrap it.