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.selectedAnswerIndex
是 nil
。通常,您不应尽可能使用隐式解包运算符 (!
),因为如果可选的是 nil
,它会使您的应用程序崩溃。相反,将其解包在 if let
语句中,并在恰好是 nil
时优雅地处理错误。
您似乎不希望 question.selectedAnswerIndex
成为 nil
,所以我认为您的代码中的其他地方可能存在逻辑错误。
问题是由于您使用视图控制器在导航控制器的 viewControllers
数组中的位置来确定问题索引引起的。 "get started" 视图控制器位于位置 0,而您的第一个 "question" 视图控制器位于位置 1。
这意味着您永远不会问有关用户食物偏好的问题“0”。
然后当您遍历问题并强制解包时 selectedAnswerIndex
您会崩溃。
一般来说,你永远不应该强制解包,除非:
- 你知道这个值不是
nil
- 无论如何你也无能为力
第二点是不要自作聪明。您可以将当前问题索引的简单 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
结构的 questionString
和 answers
属性是可选的充分理由;一个问题必须有一个问题和一组答案。我可能还会将 questionString
更改为 question
- 将类型放在 属性 名称中是多余的。
你的 QuestionController
的 tableView
属性 是使用隐式解包可选的好地方 - 你知道会有 tableview 因为你的代码首先要做的事情之一做的是创造它。使用隐式展开的可选(UITableView!
而不是 UITableView?
)将使您不必不断地展开它。
在测验的这一点上,应用程序应该计算最终分数。目前最后一题选择答案后崩溃,第三行出现标题错误:
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.selectedAnswerIndex
是 nil
。通常,您不应尽可能使用隐式解包运算符 (!
),因为如果可选的是 nil
,它会使您的应用程序崩溃。相反,将其解包在 if let
语句中,并在恰好是 nil
时优雅地处理错误。
您似乎不希望 question.selectedAnswerIndex
成为 nil
,所以我认为您的代码中的其他地方可能存在逻辑错误。
问题是由于您使用视图控制器在导航控制器的 viewControllers
数组中的位置来确定问题索引引起的。 "get started" 视图控制器位于位置 0,而您的第一个 "question" 视图控制器位于位置 1。
这意味着您永远不会问有关用户食物偏好的问题“0”。
然后当您遍历问题并强制解包时 selectedAnswerIndex
您会崩溃。
一般来说,你永远不应该强制解包,除非:
- 你知道这个值不是
nil
- 无论如何你也无能为力
第二点是不要自作聪明。您可以将当前问题索引的简单 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
结构的 questionString
和 answers
属性是可选的充分理由;一个问题必须有一个问题和一组答案。我可能还会将 questionString
更改为 question
- 将类型放在 属性 名称中是多余的。
你的 QuestionController
的 tableView
属性 是使用隐式解包可选的好地方 - 你知道会有 tableview 因为你的代码首先要做的事情之一做的是创造它。使用隐式展开的可选(UITableView!
而不是 UITableView?
)将使您不必不断地展开它。