C45.藏經閣!金庸小說知識選擇題App!
練習成果
- 可以拿來做選擇題,或問答題的題目真的太多了,這邊就拿金老的武俠小說來練習好惹,話說我最愛的是「連城訣」、「笑傲江湖」!
- 後續更新補充:透過Airtable的練習,將題庫增加至80
- 題目來源:因為ChatGPT對於金庸小說的問題錯誤很多XD
練習發想
- 多題取10: 將所有的問題進行了洗牌
(shuffle)
,然後取前10題
為當前的問題。 - 洗牌 shuffle:每次
重新開啟App
都會是不同的題目順序。不僅限於按下重新開始
按鈕,都會觸發問題洗牌。 - 選擇題:顯示
當前題目
、題目的選項
。會根據當前的索引顯示對應的問題和選項。 - 選完選項,按鈕會無效化:需要按下一頁,選項才會在啟用。
- 答對得分:如果玩家選項與正確答案匹配,那麼分數將增加,並顯示。
- 顯示得分結果:分數級距。根據玩家的得分,得到不同的
結果評語
。 - 答題顯示:會根據選項結果,給出反應音效以及文字。
- 重新開始:所有的問題將
再次被洗牌
,並重新選擇10題
問題作為當前的問題集,同時分數
也將被重置。
練習項目
主要還是要再加強對變數、物件位置的判斷以及流程邏輯的觀念,此次練習雖然用到還沒學到的知識技術,花了蠻多時間稍微理解,就先當作預習,像是outlet collection
、在viewDidLoad()
中使用IBAction方法
等。
- 自訂資料型別(struct):用來創建
題目
、選項
、正確答案
。 - for 迴圈:選擇完成後,
禁用(啟用)
所有選項按鈕、更新選項按鈕的文字。 - if else : 答案判斷、是否最後一題、分數級別的判斷。
- array:問題列表,透過append,來添加題目。
- 亂數: 透過 shuffle 來變換題目順序。
配置
黃框、紅框、紫框的內容狀態,是一同連動的,每切下一題,就會跟著變化。
- 黃框:當前第幾題。
- 紅框:題目、該題目選項( 3 個 options)。
- 藍框:評分結果顯示,一開始是隱藏。
- 灰框:目前得分狀態。
- 紫框:判斷答題對錯,給予文字提示。
- 重新開始Button:最後一題作答完畢後才會顯示。
- 下一題Button:做完題目才會顯示。
(在最後一題時不會出現下一題Button!)
Struct
這次練習的重點就是了解 strcut 的初步運用!這次三個都是 String !
這邊建立MultipleChoiceQuestion 的 strcut 用來表示選擇題的設定。包含以下屬性:
questionText
(題目文字):用來儲存題目的文字內容。options
(選項):用來儲存選項的String 陣列
。每個選項都是一個字串元素。correctAnswerText
(正確答案):用來儲存正確答案的字串。
// 選項設置
struct MultipleChoiceQuestion {
let questionText: String // 題目的文字
let options: [String] // 選項的陣列
let correctAnswerText: String // 正確答案
}
題目添加
- 使用方括號 (
[]
) 宣告陣列,裡面的元素是同一種型別。 - 建立一個空陣列(array),用來存儲
MultipleChoiceQuestion
型別的實例。
// 用於存儲題目,創建一個空的 MultipleChoiceQuestion 型別的陣列。
var questions = [MultipleChoiceQuestion]()
- 可以向
questions
陣列添加問題(MultipleChoiceQuestion)
型別所建立的問題。 - 每個題目都有對應的
題目文字
、選項
和正確答案
。這樣可以更好地組織和表示選擇題的資料。
// 依序添加題目、選項、正確答案
let question1 = MultipleChoiceQuestion(questionText: "何人曾擔任過華山派掌門?", options: ["岳不群", "梁發", "歸辛樹"], correctAnswerText: "岳不群")
questions.append(question1)
let question2 = MultipleChoiceQuestion(questionText: "周伯通被黃藥師囚於桃花島多少年?", options: ["十年", "十五年","二十年"], correctAnswerText: "十五年")
questions.append(question2)
............後面新增30題
變數運用
這些變數用於儲存和追蹤題目、顯示的題目、當前題目的索引位置以及玩家的分數。
questions
:是一個陣列,用於存儲所有的題目。currentQuestions
:也是一個陣列,用於儲存當前會顯示
的題目。類型也是MultipleChoiceQuestion
的陣列。在遊戲進行時,用來控制顯示的題目範圍。index
:用於控制當前題目的索引位置。當遊戲開始或切換到下一題時,這個變數的值會被更新,用來指示當前顯示的題目在currentQuestions
陣列中的索引位置。score
:這個變數用於追蹤玩家的分數。當玩家回答正確時,分數會增加。這個變數在遊戲中起到了記錄和追蹤分數的作用。
// 用於存儲題目
var questions = [MultipleChoiceQuestion]()
// 用來儲存會顯示出來的題目
var currentQuestions = [MultipleChoiceQuestion]()
// 控制當前題目的索引位置
var index = 0
// 分數追蹤 (答對時可以加分)
var score = 0
IBoutlet
- titleNumberLabel:會與下一題按鈕搭配。
- questionContentLabel:顯示當前題目內容 / 最後一題時會被隱藏 / 重新開始後會在顯示。
- answerResultLabel:判斷分數顯示分數級距 / 每下一題都會重置。
- scoreLabel:會在點擊選項並且答對時,加10分 / 重新開始則歸 0 。
- scoreResultLabel :預設是被隱藏 / 當最後一題時則會顯示分數結果。
// 當前第幾題
@IBOutlet weak var titleNumberLabel: UILabel!
// 題目內容
@IBOutlet weak var questionContentLabel: UILabel!
// 選項結果提示
@IBOutlet weak var answerResultLabel: UILabel!
// 分數
@IBOutlet weak var scoreLabel: UILabel!
// 結果顯示
@IBOutlet weak var scoreResultLabel: UILabel!
選項按鈕 的IBOutlet:多個元件變成 array 的 outlet collection
optionButtons
是一個UIButton
的陣列,其中包含了三個按鈕。
使用陣列來存儲按鈕:每個問題都有三個選項,而這些選項的內容是動態生成的。為了方便處理這些動態生成的選項,使用了陣列 optionButtons
。通過使用陣列,我們可以輕鬆地將選項的內容分配給對應的按鈕。
// 三個選項的 UIButton 陣列
@IBOutlet var optionButtons: [UIButton]!
- 可以很好地與
for
迴圈搭配使用。這樣就可以在迴圈中輕鬆地對選項按鈕進行操作,而不需要針對每個按鈕進行單獨的處理。
判斷正確答案:選項按鈕的 IBAction
先將三個選項按鈕的 IBAction
與 correctAnswerButton
的 IBAction
連結。
- 代碼重複使用:當玩家選擇其中一個選項按鈕時,這些按鈕的功能是相同的,
都需要進行正確答案的判斷和相應的處理
。 - 一致性和準確性:由於所有選項按鈕都使用相同的
IBAction
,表示它們功能上是一致的。這有助於保持程式的一致性,使得正確答案的處理邏輯保持統一和準確。
判斷正確答案:程式碼部分
在correctAnswerButton
中,先從 currentQuestions
陣列中取得當前題目的正確答案(correctAnswerText)
,並將它存儲在 answer
變數中。運用if else 判斷玩家選擇的按鈕的字串
是否等於正確答案 answer
。
let answer = currentQuestions[index].correctAnswerText // 宣告一個正確答案的陣列
// 判斷是否正確答案
if sender.title(for: .normal) == answer {
answerResultLabel.text = "你答對了!" // 正確:選項結果提示
score = score + 10 // 答對加10分
scoreLabel.text = "目前得分: \(score) 分"
correctSound() // 正確答案音效
} else {
answerResultLabel.text = "不要瞎猜好嗎!" // 錯誤:選項結果提示
wrongSound() // 答錯答案音效
}
- 如果符合正確答案:更新
answerResultLabel
為「你答對了!」,並將分數加 10 分。同時,分數的scoreLabel
也會被更新以顯示目前的得分。 - 如果標題不相符,表示使用者回答錯誤,程式會更新選項結果提示的標籤
answerResultLabel
為「不要瞎猜好嗎!」。 - 並且會依據選項正確與否,出現音效提示玩家。
做完選擇後,無法再選其他選項
- 因為我的程式碼一開始是將下一題直接設置成另一個Button,而不是做出選擇後,會跳到下一題。
- 因此,當初在設置選項按鈕時發現,在做出選擇後還可以再選其他選項,這樣就沒有測驗的感覺!
- 所以我讓玩家做出選擇後,就無法在對當前題目做選擇。
- 先前已經先使用 outlet collection 將
選項按鈕的IBOutlet
給放在同一個Array裡,以便在for 迴圈中一起統一操作。 - 這段程式碼的目的是禁用所有的選項按鈕,當玩家作出選擇後無法再次點擊。
- 一樣是在
correctAnswerButton
的IBAction中做設置。
// 選擇完成後,禁用所有選項按鈕,直到進入下一題或重新開始遊戲。
for button in optionButtons {
button.isEnabled = false
}
button
的變數來代表optionButtons
陣列中的每個按鈕。- 透過
for
迴圈遍歷optionButtons
陣列,程式會對陣列中的每個按鈕進行相同的操作(將它們的 isEnabled 屬性設置為 false)
,以禁用按鈕。
是否為最後一題顯示狀態(選擇按鈕與下一題按鈕的邏輯性)
在最後一題
時,當作答完畢
,不應該再出現下一題按鈕!
- 這個部分我卡很久,因為功能執行的邏輯順序沒整理清楚,導致當初我在設置時一直想說是與
下一題Button
的 IBAction 有關係。 - 一開始因為我沒將問題的關鍵給理清楚,當問了ChatGPT也沒給出適合的解法,同時我用print檢查下一頁,順序也都是對的
(因為重點根本不在當下的頁面XD)
,後來再玩過幾次遊戲後,發現重點是在作答時
的設置上,哈哈。
// 流程
答題 > 顯示下一題按鈕 > 進到下一題(隱藏下一題按鈕) > 答題>…
- (錯誤)因為我當初是將
只要有作答就會顯示出下一題
,因此只要有作答,即使是最後一題,也會顯示出下一題的按鈕。
以我的程式設置來說:
- (正確)判斷是否為最後一題的邏輯應該放在
correctAnswerButton
中而不是nextQuestionButtonPressed
。因為在correctAnswerButton
中,當用戶回答完當前題目後,我可以立即檢查是否為最後一題並相應地做出界面的顯示。 - 而在
nextQuestionButtonPressed
中,它僅在用戶按下「下一題」按鈕時觸發,所以它更適合用於處理正常的題目切換操作,而不是判斷是否為最後一題。 - 判斷是否為最後一題的邏輯應該放在
correctAnswerButton
中,以提供及時的界面顯示,而nextQuestionButtonPressed
主要負責正常的題目切換操作。
// 做出選擇
@IBAction func correctAnswerButton(_ sender: UIButton) {
// 檢查是否為最後一題
if index == currentQuestions.count - 1 {
restartButton.isHidden = false // 是最後一題,顯示 restartButton按鈕
questionContentLabel.isHidden = true // 是最後一題,隱藏 題目
getScoreResultText() // 取得評分結果
} else {
nextButton.isHidden = false // 不是最後一題,顯示nextButton按鈕
}
}
// 下一題Button
@IBAction func nextQuestionButtonPressed(_ sender: UIButton) {
// 檢查是否超出問題範圍
if index < currentQuestions.count - 1 {
index = index + 1 // 增加 index 值
setupQuestion() // 題目相關 function
}
// 停止音效
soundPlayer.pause()
}
- 我還被安慰了!?
錯誤的部分:在最後一題,作答完畢後,還會有下一題按鈕的出現。
錯誤程式碼與邏輯:
- correctAnswerButton部分:我將
nextButton.isHidden
分別放在if else 的答對答錯的判斷裡
,因此只要有作答就一定會將nextButton
顯示出來。 - nextQuestionButtonPressed部分:只要按下
下一題的按鈕
,下一題的按鈕
在下一題出現就會被隱藏。
// 錯誤流程
答題 > 顯示下一題按鈕 > 進到下一題(隱藏下一題按鈕) > 最後一題 > 答題 > 顯示下一題(因為只要有作答就會跑出nextButton)
錯誤程式碼
// 做出選擇
@IBAction func correctAnswerButton(_ sender: UIButton) {
let answer = currentQuestions[index].correctAnswerText // 宣告一個正確答案的陣列
// 判斷是否正確答案
if sender.title(for: .normal) == answer {
optionsResultLabel.text = "你答對了!" // 正確:選項結果提示
score = score + 10
scoreLabel.text = "目前得分: \(score) 分"
// (錯誤)
nextButton.isHidden = false
} else {
optionsResultLabel.text = "不要瞎猜好嗎!" // 錯誤:選項結果提示
// (錯誤)
nextButton.isHidden = false
}
}
// 下一題
@IBAction func nextQuestionButtonPressed(_ sender: UIButton) {
// 檢查是否超出問題範圍
if index < currentQuestions.count - 1 {
// 增加 index 值
index = index + 1
// 隱藏nextbutton (錯誤)
nextButton.isHidden = true
} else {
index = currentQuestions.count - 1
nextButton.isHidden = true
// 啟用 重新開始功能
restartButton.isHidden = false
}
請ChatGPT解釋:知道問題後在詢問果然蠻準確的。
重新啟動、多題取10題
// 重新開始Button
@IBAction func restartGameButtonPressed(_ sender: UIButton) {
questions.shuffle() // 隨機打亂所有問題的順序
currentQuestions = Array(questions.prefix(10)) // 只取前10題(洗牌後)
index = 0 // 重置索引值
setupQuestion() // 題目相關 function
score = 0 // 分數重置
scoreLabel.text = "目前得分: 0 分" // 計分欄位重置
questionContentLabel.isHidden = false // 因為最後一題會隱藏 題目欄 ,重新開始後,題目顯示
scoreResultLabel.text = "" // 清空評分結果內容
scoreResultLabel.isHidden = true // 重新開始後,會將評分欄給隱藏,以便題目欄可以正常顯示
restartButton.isHidden = true // 重新開始後,重置按鈕隱藏
soundPlayer.pause() // 停止音效
}
先對所有題目洗牌.shuffle
questions.shuffle() // 隨機打亂所有問題的順序
再從洗牌後的所有題目取前10題
透過prefix(_:)
來取questions
陣列中的前10題,並給予currentQuestions
陣列。
currentQuestions = Array(questions.prefix(10)) // 只取前10題(洗牌後)
備註:啟動App時就隨機生成題目
- 也因為將
restartGameButtonPressed
放在viewDidLoad
中,使得在 App啟動時(即當viewDidLoad
執行時),也會執行這個函式。 - 讓 App 啟動時就會生成一組隨機的題目。
初始狀態 (每次啟動App時,題目都會不同)
- initQuestions():是問題列表,包括問題文本,選項,和正確答案。
- restartGameButtonPressed(restartButton):由於題目列表一共有30題,而我只會從中出10題。
- 已經在
restartGameButtonPressed
裡面設置了先洗牌,在取前10題。 - 通過傳遞
restartButton
參數,該函式會在遊戲開始
時立即被呼叫,以確保每次重新啟動App
時,題目都不同。
override func viewDidLoad() {
super.viewDidLoad()
initQuestions() // 題目、選項、正確答案列表
restartButton.isHidden = true // 預設:重新開始Button隱藏,到最後一題,才會顯示。
nextButton.isHidden = true // 預設:nextButton先隱藏,必須做出選擇才會顯示。
scoreResultLabel.isHidden = true // 預設:隱藏得分結果顯示
restartGameButtonPressed(restartButton) // 調用重新開始函數來進行題目洗牌和畫面設定
}
function:題目及選項設置、nextButton隱藏、題目提示
// 題目設置相關(問題、選項)、 nextButton隱藏、題目提示
func setupQuestion() {
// 更新題目內容
questionContentLabel.text = currentQuestions[index].questionText
// 更新選項按鈕的標題,顯示當前文字 (問題的選項數量),根據當前問題的選項數量,來設定選項按鈕的標題
for i in 0..<currentQuestions[index].options.count {
optionButtons[i].setTitle(currentQuestions[index].options[i], for: .normal)
}
// 啟用所有選項按鈕,準備開始新的一題。
for button in optionButtons {
button.isEnabled = true
}
nextButton.isHidden = true // 隱藏nextbutton
titleNumberLabel.text = "第\(index+1)/\(currentQuestions.count)題" // 當前第幾題
answerResultLabel.text = "答題結果" // 更新 選項結果提示
}
更新題目內容
- 將
questionContentLabel
的文字內容設定為當前題目的文本
。 currentQuestions
是包含當前顯示的題目的陣列,index
則是控制當前題目的索引位置,所以currentQuestions[index]
就是當前題目的資訊,而questionText
是該題目的問題文本。
// 更新題目內容
questionContentLabel.text = currentQuestions[index].questionText
更新當前題目的選項按鈕
// 更新選項按鈕的標題,顯示當前文字 (問題的選項數量),根據當前問題的選項數量,來設定選項按鈕的標題
for i in 0..<currentQuestions[index].options.count {
optionButtons[i].setTitle(currentQuestions[index].options[i], for: .normal)
}
- for i in 0..<currentQuestions[index].options.count {}
- 用於遍歷當前題目的選項,由於陣列的索引是從 0 開始的,使用
0..<
。 currentQuestions[index].options
是一個陣列,其中包含了當前題目的所有選項內容。
2. optionButtons[i].setTitle(currentQuestions[index].options[i], for: .normal):
- 根據當前題目的選項數量,將選項按鈕的標題設置為相應的選項內容。
optionButtons
是一個包含選項按鈕的陣列。currentQuestions[index].options
是一個陣列,包含當前題目的選項內容。i
是迴圈的索引,代表當前選項按鈕在陣列中的位置。
此程式碼會遍歷 0..<currentQuestions[index].options.count
的範圍,逐個取出當前題目的選項內容,並將其設置為相應選項按鈕的標題。
當前第幾題
- 由於index是由0開始,因此要 index + 1。
- 先前在
restartGameButtonPressed
以設置currentQuestions
的 count,因此取10題是由他的count屬性來取數值。
titleNumberLabel.text = "第\(index+1)/\(currentQuestions.count)題" // 當前第幾題
分數區間
根據玩家的得分來設置不同的評分結果。根據分數的不同範圍,會顯示不同的結果在 scoreResultLabel
中。
- 宣告一個
result
變數,用來存儲根據分數所對應的評分結果。 - 使用
if-else
來判斷分數的範圍,根據不同的分數範圍,result
也會顯示不同內容。 - 最後,將
得分結果
和評分結果
以字串的形式組合起來,並設置給scoreResultLabel.text
。 - 同時,將
scoreResultLabel
設置為可見狀態,以顯示評分結果。
// 取得評分標準
func getScoreResultText() {
let result: String // 根據分數設置不同的結果
if score < 20 {
result = "大俠還是自盡吧!"
} else if score < 40 {
result = "資質駑鈍!再回去翻小說吧!"
} else if score < 60 {
result = "資質平庸!梁發是你!?"
} else if score < 80 {
result = "少林廚藝訓練學院歡迎您!"
} else {
result = "你有吃記憶吐司嗎?"
}
scoreResultLabel.text = "最終成績為:\(score) 分" + "\n" + result // 是最後一題,顯示 評分結果
scoreResultLabel.isHidden = false // 是最後一題,顯示 評分欄
}
這邊再次加強一下對於括弧的認知:
GitHub
(運用CodableCSV 解析 csv)
參考