C45.藏經閣!金庸小說知識選擇題App!

練習成果

  • 可以拿來做選擇題,或問答題的題目真的太多了,這邊就拿金老的武俠小說來練習好惹,話說我最愛的是「連城訣」、「笑傲江湖」!
有加入答題音效@@
  • 後續更新補充:透過Airtable的練習,將題庫增加至80
  • 題目來源:因為ChatGPT對於金庸小說的問題錯誤很多XD

練習發想

  1. 多題取10: 將所有的問題進行了洗牌(shuffle),然後取前10題為當前的問題。
  2. 洗牌 shuffle:每次重新開啟App都會是不同的題目順序。不僅限於按下重新開始按鈕,都會觸發問題洗牌。
  3. 選擇題:顯示當前題目、題目的選項。會根據當前的索引顯示對應的問題和選項。
  4. 選完選項,按鈕會無效化:需要按下一頁,選項才會在啟用。
  5. 答對得分:如果玩家選項與正確答案匹配,那麼分數將增加,並顯示。
  6. 顯示得分結果:分數級距。根據玩家的得分,得到不同的結果評語
  7. 答題顯示:會根據選項結果,給出反應音效以及文字。
  8. 重新開始:所有的問題將再次被洗牌,並重新選擇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題

變數運用

這些變數用於儲存和追蹤題目、顯示的題目、當前題目的索引位置以及玩家的分數。

  1. questions:是一個陣列,用於存儲所有的題目。
  2. currentQuestions:也是一個陣列,用於儲存當前會顯示的題目。類型也是 MultipleChoiceQuestion 的陣列。在遊戲進行時,用來控制顯示的題目範圍。
  3. index:用於控制當前題目的索引位置。當遊戲開始或切換到下一題時,這個變數的值會被更新,用來指示當前顯示的題目在 currentQuestions 陣列中的索引位置。
  4. 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

先將三個選項按鈕的 IBActioncorrectAnswerButtonIBAction 連結。

  • 代碼重複使用:當玩家選擇其中一個選項按鈕時,這些按鈕的功能是相同的,都需要進行正確答案的判斷和相應的處理
  • 一致性和準確性:由於所有選項按鈕都使用相同的 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()
}
  • 我還被安慰了!?

錯誤的部分:在最後一題,作答完畢後,還會有下一題按鈕的出現。

錯誤:當第10題時,還會出現nextButton

錯誤程式碼與邏輯:

  • 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)
}
  1. 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)

參考

--

--

wei Tsao 學習紀錄
彼得潘的 Swift iOS / Flutter App 開發教室

Hi ! 我是wei , 先前未接觸過程式開發設計,想藉此來記錄自己的學習歷程,以利培養自己的程式邏輯 :)