Swift:应用程序崩溃并出现 EXC_BAD_INSTRUCTION 错误?

Swift: app crashes with EXC_BAD_INSTRUCTION error?

在测验的这一点上,应用程序应该计算最终分数。目前最后一题选择答案后崩溃,第三行出现标题错误:

var score = 0
for question in QuestionController.questionsList {
score += question.selectedAnswerIndex!

下面是该文件的完整代码。它在应用程序的先前迭代中运行良好,但今天才开始。我知道 force-unwrapping 选项有问题,但不清楚为什么它只出现在这种情况下:

    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)

}

}

它正在崩溃,因为 question.selectedAnswerIndexnil。通常,您不应尽可能使用隐式解包运算符 (!),因为如果可选的是 nil,它会使您的应用程序崩溃。相反,将其解包在 if let 语句中,并在恰好是 nil 时优雅地处理错误。

您似乎不希望 question.selectedAnswerIndex 成为 nil,所以我认为您的代码中的其他地方可能存在逻辑错误。

问题是由于您使用视图控制器在导航控制器的 viewControllers 数组中的位置来确定问题索引引起的。 "get started" 视图控制器位于位置 0,而您的第一个 "question" 视图控制器位于位置 1。

这意味着您永远不会问有关用户食物偏好的问题“0”。

然后当您遍历问题并强制解包时 selectedAnswerIndex 您会崩溃。

一般来说,你永远不应该强制解包,除非:

  1. 你知道这个值不是 nil
  2. 无论如何你也无能为力

第二点是不要自作聪明。您可以将当前问题索引的简单 int 传递给视图控制器。虽然这不像 "clever" 那样使用 viewControllers 数组来确定问题索引,但如果堆栈中的视图控制器数量增加,则更容易看到正在发生的事情并且不会中断变化。

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)
       }
    }
}

这段代码不仅有效,而且更容易看到正在发生的事情,而且代码行数更少,因为您不需要一直从视图控制器数组中解包可选索引;例如,numberOfRowsInSection 从 7 行变为 1。

此外,在这种情况下,我看不出 Question 结构的 questionStringanswers 属性是可选的充分理由;一个问题必须有一个问题和一组答案。我可能还会将 questionString 更改为 question - 将类型放在 属性 名称中是多余的。

你的 QuestionControllertableView 属性 是使用隐式解包可选的好地方 - 你知道会有 tableview 因为你的代码首先要做的事情之一做的是创造它。使用隐式展开的可选(UITableView! 而不是 UITableView?)将使您不必不断地展开它。