【iOS】#5 選擇題App|英文單字測驗

製作功能:

  • MVC 架構,自訂 view controller 類別 & model 型別
  • 選擇題 App,每題有四個選項。
  • 答對一題加 10 分 。
  • 畫面上顯示目前題目是第幾題。
  • 自訂選擇題的資料型別。
  • 題庫有 n 題,隨機出其中的 10 題,每次玩的時候題目順序都不一樣。( n > 10 )
  • 包含問題頁面和分數頁面。
  • 頁面間傳遞資料,將結果從問題頁傳到分數頁。
  • 用 UIAlertController 顯示答錯,在 alert 裡顯示正確答案。

UI的部分就不贅述,詳細程式碼可以在 GitHub 上查看(文章最後會附上連結),以下挑一些重點來介紹~

Modal — Question

自訂選擇題的資料型別

struct Question {
let text: String
let answerIndex: Int
let options: [String]
}

let questions: [Question] = [
Question(
text: "Abundance",
answerIndex: 1,
options: ["放棄", "豐富", "慷慨", "貧困"]
),
Question(
text: "Crisis",
answerIndex: 3,
options: ["機會", "成就", "挑戰", "危機"]
),
Question(
text: "Diligent",
answerIndex: 2,
options: ["優雅", "忠誠", "勤奮", "耐心"]
),
// 添加n個題目(n>10)...
]

ViewController

在系統做好的 ViewController中先 present 一個 UINavigationController出來,將 QnaViewController設置為其 rootViewController

class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let qnaViewController = QnaViewController()
let navigationController = UINavigationController(rootViewController: qnaViewController)
navigationController.modalPresentationStyle = .fullScreen
present(navigationController, animated: false)
// 設置導航欄返回按鈕的文字
navigationController.navigationBar.tintColor = .brown
// 設置導航欄返回按鈕的顏色
qnaViewController.navigationItem.backBarButtonItem = UIBarButtonItem(title: "返回", style: .plain, target: nil, action: nil)
}
}

QnaViewController

1. 隨機出其中的 10 題

使用 shuffle() 對 Array 做隨機排序

class QnaViewController: UIViewController {

var newQuestions = [Question]()
var optionButtons = [UIButton]()
var index: Int = 0

override func viewDidLoad() {
super.viewDidLoad()
questionRandom()
updateUI()
}

func questionRandom() {
newQuestions.removeAll()
var questions = questions
questions.shuffle()
for index in 1...10 {
newQuestions.append(questions[index])
}
}

func updateUI() {
let question = newQuestions[index]
//題目文字
questionLabel.text = question.text
//選項按鈕文字、顏色
let options = question.options
for index in options.indices {
optionButtons[index].setTitle(options[index], for: .normal)
optionButtons[index].backgroundColor = .lightBrown
}
//目前在第幾題
questionNumber.text = "\(index + 1)/10"
}
}

2. 點擊選項時判斷答案是否正確,正確亮綠燈並加分,錯誤亮紅燈

使用 firstIndex(of:) 方法查找發送動作的按鈕在 Array 中的 index

class QnaViewController: UIViewController {

var score: Int = 0

@objc func tabButton(_ sender: UIButton) {
let question = newQuestions[index]
//查找發送動作的按鈕在Array中的index
guard let optionChosen = optionButtons.firstIndex(of: sender) else { return }
//答對
if optionChosen == question.answerIndex {
sender.backgroundColor = .correctGreen
score += 10
} else {
//答錯
sender.backgroundColor = .wrongRed
}
}
}

3. 用 UIAlertController 顯示答錯,在 alert 裡顯示正確答案

@objc func tabButton(_ sender: UIButton) {
//...

//答錯
sender.backgroundColor = .wrongRed
let controller = UIAlertController(title: "答錯了!", message: "正確答案是:\(question.options[question.answerIndex])", preferredStyle: .alert)
let continueAction = UIAlertAction(title: "繼續", style: .default, handler: nil)
controller.addAction(continueAction)
present(controller, animated: true)
}

4. 進入下一題、延遲任務

  • 答對時:使用 DispatchQueue.main.asyncAfter 延遲進入下一題,避免看不到選項亮綠燈。
  • 答錯時:延遲 present UIAlertController ,同時將正確答案亮綠燈。點選 UIAlertAction “繼續” 後,直接進入下一題。
@objc func tabButton(_ sender: UIButton) {
//...

//答對
if optionChosen == question.answerIndex {
sender.backgroundColor = .correctGreen
score += 10
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self else { return }
self.next()
}
} else {
//答錯
sender.backgroundColor = .wrongRed
let controller = UIAlertController(title: "答錯了!", message: "正確答案是:\(question.options[question.answerIndex])", preferredStyle: .alert)
let continueAction = UIAlertAction(title: "繼續", style: .default) { [weak self] _ in
guard let self else { return }
self.next()
}
controller.addAction(continueAction)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
guard let self else { return }
present(controller, animated: true)
optionButtons[question.answerIndex].backgroundColor = .correctGreen
}
}
}

func next() {
if index < 9 {
index += 1
updateUI()
}
}

補充 DispatchQueue.main.asyncAfter:

在主執行緒上非同步執行一個延遲任務,加上指定的秒數後執行閉包中的程式碼。

補充 weak self:

在閉包中使用了 [weak self] 來避免循環引用,並且使用了 guard let self else { return } 來確保在閉包執行時 self 仍然存在,如果 self 已經被釋放了,那麼閉包中的程式碼就不會執行,從而避免了潛在的崩潰或記憶體洩漏問題。

5. 作答最後一題後,將分數從問題頁傳到分數頁,並切換至分數頁

判斷是否為最後一題,若為最後一題則切換至分數頁

if isLastQuestion(index: index) {
presentResult()
}

在進入下一題前(呼叫 self.next() 前),加入上述 if 判斷式

@objc func tabButton(_ sender: UIButton) {
//...

//答對
if optionChosen == question.answerIndex {
sender.backgroundColor = .correctGreen
score += 10
if isLastQuestion(index: index) {
presentResult()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
guard let self else { return }
self.next()
}
} else {
//答錯
sender.backgroundColor = .wrongRed
let controller = UIAlertController(title: "答錯了!", message: "正確答案是:\(question.options[question.answerIndex])", preferredStyle: .alert)
let continueAction = UIAlertAction(title: "繼續", style: .default) { [weak self] _ in
guard let self else { return }
if self.isLastQuestion(index: index) {
self.presentResult()
}
self.next()
}
controller.addAction(continueAction)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
guard let self else { return }
present(controller, animated: true)
optionButtons[question.answerIndex].backgroundColor = .correctGreen
}
}
}

將分數從問題頁傳到分數頁,使用 navigationController.pushViewController 切換至分數頁

func isLastQuestion(index: Int) -> Bool {
return index == 9
}

func presentResult() {
let alertController = UIAlertController(title: "作答完畢", message: "快來看看你的分數吧!", preferredStyle: .alert)
let continueAction = UIAlertAction(title: "確定", style: .default) { [weak self] _ in
guard let self else { return }
let scoreViewController = ScoreViewController()
//將分數從問題頁傳到分數頁
scoreViewController.score = self.score
self.navigationController?.pushViewController(scoreViewController, animated: true)
}
alertController.addAction(continueAction)
present(alertController, animated: true)
}

ScoreViewController

1. 取得分數資料

建立 score 變數,在上述QnaViewController func presentResult中取得分數資料,並呈現對應結果。

class ScoreViewController: UIViewController {

var score: Int?

override func viewDidLoad() {
super.viewDidLoad()
getResult()
}

func getResult() {
guard let score = score else { return }
scoreLabel.text = "\(score)"
if score == 100 {
result.text = "恭喜!全部答對囉🥳"
} else if score >= 60, score < 100 {
result.text = "差一點了,不熟的單字再加強!"
} else if score < 60 {
result.text = "錯太多了,回去重背!"
}
}
}

2. 點擊重新開始

使用 navigationController.popViewController 返回問題頁

@objc func restartPressed(_ sender: UIButton) {
navigationController?.popViewController(animated: true)
}

QnaViewController viewWillAppear 時,重置 index 和分數,問題隨機排序後 update UI

class QnaViewController: UIViewController {
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
index = 0
score = 0
questionRandom()
updateUI()
}
}

APP 操作展示:

--

--