swiftPractice[21]_選擇題APP-韓國流行語

Tania
彼得潘的 Swift iOS / Flutter App 開發教室
24 min readJan 2, 2024

上一次去韓國已經是5年前了,藉這著作業來複習一下生疏的韓文。過程中重複測試的次數之多,整個作業做完還真的全部都背下來了😂

APP功能要求 :

• 每題有四個選項
• 定義題庫的 array
• 自訂資料型別
• 答對一題加 10 分
• 畫面上顯示目前題目是第幾題
• 題庫有 n 題,隨機出其中的 10 題,每次玩的時候題目順序都不一樣。( n > 10 )
• 玩完後可選擇再玩一次,重新開始玩
• 分成問題頁跟分數頁,將結果從問題頁傳到分數頁。利用 IBSegueAction & PerformSegue
• 使用 UIAlertController

拆解!

<目錄>
1.畫面規劃、拉 IBOutlet & IBAction
2.定義題庫 array
• 自定資料型別
3.設定初始畫面
4.自訂 function
• unlockButtons(_ lock:Bool)
• updateUI()
5.IBAction 功能一:selectAnswer(_ sender: UIButton)
• 用 if else 判斷答案對錯
• 答錯跳出 UIAlertController
• 分數頁面跳轉:10題作答完畢後
6.IBAction 功能二:tapForNext(_ sender: UIButton)
7.IBAction 功能三:unwindToQuestion(_ unwindSegue: UIStoryboardSegue)

1. 畫面規劃、拉 IBOutlet & IBAction

 //QuestionViewController  
@IBOutlet weak var questionLabel: UILabel!
@IBOutlet weak var questionNumberLabel: UILabel!
@IBOutlet weak var scoreLabel: UILabel!

@IBOutlet var optionButtons: [UIButton]!
@IBOutlet weak var nextButton: UIButton!

@IBOutlet weak var progressBar: UIProgressView!

//ResultViewController
@IBOutlet weak var totalScoreLabel: UILabel!
左:QuestionViewController/右:ResultViewController

2. 定義題庫 array

  • 自訂資料型別:建立一個新的 Swift File 存放自訂的 Struct 型別
  • 回到問題主頁面 QuestionViewController 建立題庫 array
    將自定的 Question 型別代入 array,然後就可以得到方便的輸入格式如下圖
 var questions:[Question] = [
Question(question: "좋못사", options: ["非常愛","很好","廚師","魔術師"], answer: 1, answerDetail: "原文意思為「좋아하다 못해 사랑해(沒辦法喜歡你,只能愛你)」"),
Question(question: "삼귀다", options: ["交往","三個鬼","曖昧","喜歡"], answer: 3, answerDetail: "原本交往的韓文是사귀다,而사音同韓文的四,但還沒到交往的四,那就是曖昧的三,所以就會用삼귀다來說。"),
Question(question: "혼틈", options: ["單身","趁亂","一個人","空閒"], answer: 2, answerDetail: "혼란을 틈다(趁混亂的空隙)的縮短語"),
Question(question: "핑프", options: ["企鵝","伸手牌","乒乓球","粉紅色"], answer: 2, answerDetail: "핑거 프린세스(Finger Princess ),指的是明明可以自己找到答案,卻愛問別人的人。"),
Question(question: "많관부", options: ["很多","滿貫全壘打","請多關(照)","很多觀眾"], answer: 3, answerDetail: "많은 관심 부탁드립니다(請多多關照),主要是藝人在SNS發文很常用到的話,屬於輕鬆的語氣。"),
Question(question: "팩폭", options: ["真(相)暴(力)","臉書","貼面膜","好朋友"], answer: 1, answerDetail: "팩트폭행(Fact暴行),意味著「給予真相的暴力」,也就是直接挑名真相,帶給人衝擊。"),
Question(question: "비담", options: ["顏(值)擔(當)","血汗","下雨","飛彈"], answer: 1, answerDetail: "비주얼 담당(顏值擔當)的意思,而「비주얼」原意為「Visual」視覺,負責視覺的人一定是最好看的,因此就有這個流行語的誕生。"),
Question(question: "별다줄", options: ["全部給我","全部減少","彈珠","什麼都要縮寫"], answer: 4, answerDetail: "原意為「별걸 다 줄인다(什麼都要縮寫)」,就是在說這樣的縮寫、流行語文化。"),
Question(question: "오히려 좋아", options: ["很喜歡","竟然喜歡","偶爾很好","反而更好"], answer: 4, answerDetail: "反正壞事已經發生,之後就會有好事發生,韓國人常將它與「가보자고(試試看吧)」一起使用,更增強了積極的意志。"),
Question(question: "어쩔티비 저쩔티비", options: ["電視品牌","那裡有電視","沒事做","你想怎樣"], answer: 4, answerDetail: "어쩌라고 가서 TV나 봐(不然想怎樣)的意思,後面的TV其實沒什麼含意,可以換成各種電子產品或其他昂貴的東西。"),
Question(question: "뇌절", options: ["節慶","腦節","內部調節","聰明"], answer: 2, answerDetail: "用來表示思維停止"),
Question(question: "점메추", options: ["白菜","午(餐)菜(單)推(薦)","從來沒出現過","店家推薦"], answer: 2, answerDetail: "점메추(午餐菜單推薦)/ 저메추(晚餐菜單推薦)"),
Question(question: "스불재", options: ["23","師徒制","自作自受","失火"], answer: 3, answerDetail: "스스로 불러온 재앙(自己招來的災殃)"),
Question(question: "갑통알", options: ["突然想吃蛋","價格公開","突(然看了)存(摺要去打)工","突然理解"], answer: 3, answerDetail: "指的是「突然看了存摺要去打工」,是大學生們間常常說的語句,代表沒錢了。"),
Question(question: "마기꾼", options: ["騎士","口罩騙子","小偷","媽寶"], answer: 2, answerDetail: "口罩(마스크)加上騙子(사기꾼)的合成語,意思是指口罩拿下的前後差異很大。"),
]

3. 設定初始畫面

  • 宣告需要的變數
    var score:Int = 0
var index:Int = 0
  • viewDidLoad( )
    使用 shuffle( ) 改變題庫順序,讓每次新的一局題目順序不同
override func viewDidLoad() {
super.viewDidLoad()
//shuffle重組題目array
questions.shuffle()
score = 0
index = 0
scoreLabel.text = "0"
updateUI()
}

4. 自訂 function

  • unlockButtons(_ lock:Bool)
    當使用者作答完畢,鎖住選項按鈕,避免重複選擇答案
    func unlockButtons(_ lock:Bool){
for i in 0...3{
optionButtons[i].isUserInteractionEnabled = lock
}
}
  • updateUI( )
    設定每新的一題需要的介面狀態、復原上一題作答後的元件改變
    func updateUI(){
index += 1
questionNumberLabel.text = "第 \(index) 題"
questionLabel.text = questions[index].question
//讓選項按鍵可以按、隱藏下一題按鍵
unlockButtons(true)
nextButton.isHidden = true
//設定進度條
progressBar.progress = Float(index)/10
//設定選項按鈕顏色與內容
for i in 0...3 {
optionButtons[i].configuration?.baseBackgroundColor = .systemCyan
optionButtons[i].setTitle(questions[index].options[i], for: .normal)
}
}

5. IBAction 功能一:selectAnswer(_ sender: UIButton)

  • 用 if else 判斷答案對錯
    如果選擇的答案等於當前題目的答案代表答對;其餘則答錯
@IBAction func selectAnswer(_ sender: UIButton) {

let currentQuestion = questions[index]
let currentAnswer = currentQuestion.options[currentQuestion.answer - 1]

//答對
if sender.titleLabel?.text == currentAnswer{
//對的答案按鍵變綠色
sender.configuration?.baseBackgroundColor = .systemGreen
//答對加十分
score += 10
scoreLabel.text = "\(score)"
//限制選擇鍵重複選擇
unlockButtons(false)

//判斷第十題作答完畢
if index == 10{
//跳轉到分數頁面
performSegue(withIdentifier: "resultPage", sender: nil)
}else{
//未滿十題出現下一題按鈕,需按按鈕到下一題作答
nextButton.isHidden = false
}
//答錯
}else{
//錯的答案按鍵變紅色
sender.configuration?.baseBackgroundColor = .systemRed
//用UIAlertController公布正確答案與解釋
let controller = UIAlertController(title: currentAnswer, message: currentQuestion.answerDetail, preferredStyle: .alert)
//新增okAction
let okAction = UIAlertAction(title: "了解!", style: .default) { [self] action in
//判斷第十題作答完畢
if index == 10{
//跳轉到分數頁面
performSegue(withIdentifier: "resultPage", sender: nil)
}else{
//未滿十題直接進入下一題
updateUI()
}
}
controller.addAction(okAction)
present(controller, animated: true)
}
}
  • 答錯跳出 UIAlertController
    在 closure 裡寫按下okAction 需要執行的程式
//用UIAlertController公布正確答案與解釋
let controller = UIAlertController(title: currentAnswer, message: currentQuestion.answerDetail, preferredStyle: .alert)
//新增okAction
let okAction = UIAlertAction(title: "了解!", style: .default) { [self] action in
//判斷第十題作答完畢
if index == 10{
//跳轉到分數頁面
performSegue(withIdentifier: "resultPage", sender: nil)
}else{
//未滿十題直接進入下一題
updateUI()
}
}

controller.addAction(okAction)
present(controller, animated: true)
答錯跳出 UIAlertController
  • 分數頁面跳轉:10題作答完畢後
    先在 QuestionViewController(起點)拉好 IBSegueAction 並記得設定 ID

到 ResultViewController 定義要接收資料傳遞的變數,並將其更新到畫面上

以上完成後,就可以在需要時使用 performSegue 控制頁面跳轉

performSegue(withIdentifier: "resultPage", sender: nil)

✦ 補充:使用 Navigation Controller 連接頁面時,會出現 <Back 字樣,沒有辦法直接點選刪除,但又只希望使用者透過設計好的 Replay 按鍵回到前一頁重玩。
解決方法:
從程式將 navigationItem 裡的 .hidesBackButton 設定成 true 將他隱藏!

6. IBAction 功能二:tapForNext(_ sender: UIButton)

下一題按鍵:答對時出現,按下可進入下一題

 @IBAction func tapForNext(_ sender: UIButton) {
updateUI()
}
答對時出現下一題按鍵

7. IBAction 功能三:unwindToQuestion(_ unwindSegue: UIStoryboardSegue)

分數頁面要重玩時可按按鍵回到上一頁:
在 QuestionViewController 定義 unwind IBAction

    @IBAction func unwindToQuestion(_ unwindSegue: UIStoryboardSegue) {
_ = unwindSegue.source
viewDidLoad()
}
}

從 button 拉線連到 Exit,會出現剛剛定義好的 IBAction: unwindToQuestion

以上設定完畢就可以回到上一頁,在 unwindToQuestion 裡呼叫 viewDidLoad( ),就可以達到回上一頁並重新開始遊戲的功能啦!

Replay 按鍵回到上一頁

完整程式碼

QuestionViewController

import UIKit

class QuestionViewController: UIViewController {

@IBOutlet weak var questionLabel: UILabel!
@IBOutlet weak var questionNumberLabel: UILabel!
@IBOutlet weak var scoreLabel: UILabel!

@IBOutlet var optionButtons: [UIButton]!

@IBOutlet weak var nextButton: UIButton!

@IBOutlet weak var progressBar: UIProgressView!
var score:Int = 0
var index:Int = 0

var questions:[Question] = [
Question(question: "좋못사", options: ["非常愛","很好","廚師","魔術師"], answer: 1, answerDetail: "原文意思為「좋아하다 못해 사랑해(沒辦法喜歡你,只能愛你)」"),
Question(question: "삼귀다", options: ["交往","三個鬼","曖昧","喜歡"], answer: 3, answerDetail: "原本交往的韓文是사귀다,而사音同韓文的四,但還沒到交往的四,那就是曖昧的三,所以就會用삼귀다來說。"),
Question(question: "혼틈", options: ["單身","趁亂","一個人","空閒"], answer: 2, answerDetail: "혼란을 틈다(趁混亂的空隙)的縮短語"),
Question(question: "핑프", options: ["企鵝","伸手牌","乒乓球","粉紅色"], answer: 2, answerDetail: "핑거 프린세스(Finger Princess ),指的是明明可以自己找到答案,卻愛問別人的人。"),
Question(question: "많관부", options: ["很多","滿貫全壘打","請多關(照)","很多觀眾"], answer: 3, answerDetail: "많은 관심 부탁드립니다(請多多關照),主要是藝人在SNS發文很常用到的話,屬於輕鬆的語氣。"),
Question(question: "팩폭", options: ["真(相)暴(力)","臉書","貼面膜","好朋友"], answer: 1, answerDetail: "팩트폭행(Fact暴行),意味著「給予真相的暴力」,也就是直接挑名真相,帶給人衝擊。"),
Question(question: "비담", options: ["顏(值)擔(當)","血汗","下雨","飛彈"], answer: 1, answerDetail: "비주얼 담당(顏值擔當)的意思,而「비주얼」原意為「Visual」視覺,負責視覺的人一定是最好看的,因此就有這個流行語的誕生。"),
Question(question: "별다줄", options: ["全部給我","全部減少","彈珠","什麼都要縮寫"], answer: 4, answerDetail: "原意為「별걸 다 줄인다(什麼都要縮寫)」,就是在說這樣的縮寫、流行語文化。"),
Question(question: "오히려 좋아", options: ["很喜歡","竟然喜歡","偶爾很好","反而更好"], answer: 4, answerDetail: "反正壞事已經發生,之後就會有好事發生,韓國人常將它與「가보자고(試試看吧)」一起使用,更增強了積極的意志。"),
Question(question: "어쩔티비 저쩔티비", options: ["電視品牌","那裡有電視","沒事做","你想怎樣"], answer: 4, answerDetail: "어쩌라고 가서 TV나 봐(不然想怎樣)的意思,後面的TV其實沒什麼含意,可以換成各種電子產品或其他昂貴的東西。"),
Question(question: "뇌절", options: ["節慶","腦節","內部調節","聰明"], answer: 2, answerDetail: "用來表示思維停止"),
Question(question: "점메추", options: ["白菜","午(餐)菜(單)推(薦)","從來沒出現過","店家推薦"], answer: 2, answerDetail: "점메추(午餐菜單推薦)/ 저메추(晚餐菜單推薦)"),
Question(question: "스불재", options: ["23","師徒制","自作自受","失火"], answer: 3, answerDetail: "스스로 불러온 재앙(自己招來的災殃)"),
Question(question: "갑통알", options: ["突然想吃蛋","價格公開","突(然看了)存(摺要去打)工","突然理解"], answer: 3, answerDetail: "指的是「突然看了存摺要去打工」,是大學生們間常常說的語句,代表沒錢了。"),
Question(question: "마기꾼", options: ["騎士","口罩騙子","小偷","媽寶"], answer: 2, answerDetail: "口罩(마스크)加上騙子(사기꾼)的合成語,意思是指口罩拿下的前後差異很大。"),
]

func updateUI(){
index += 1
questionNumberLabel.text = "第 \(index) 題"
questionLabel.text = questions[index].question
//讓選項按鍵可以按、隱藏下一題按鍵
unlockButtons(true)
nextButton.isHidden = true
//設定進度條
progressBar.progress = Float(index)/10
//設定選項按鈕顏色與內容
for i in 0...3 {
optionButtons[i].configuration?.baseBackgroundColor = .systemCyan
optionButtons[i].setTitle(questions[index].options[i], for: .normal)
}
}
func unlockButtons(_ lock:Bool){
for i in 0...3{
optionButtons[i].isUserInteractionEnabled = lock
}
}

override func viewDidLoad() {
super.viewDidLoad()
//shuffle重組題目array
questions.shuffle()
score = 0
index = 0
updateUI()
scoreLabel.text = "0"
}

@IBAction func selectAnswer(_ sender: UIButton) {

let currentQuestion = questions[index]
let currentAnswer = currentQuestion.options[currentQuestion.answer - 1]

//答對
if sender.titleLabel?.text == currentAnswer{
//對的答案按鍵變綠色
sender.configuration?.baseBackgroundColor = .systemGreen
//答對加十分
score += 10
scoreLabel.text = "\(score)"
//限制選擇鍵重複選擇
unlockButtons(false)

//判斷第十題作答完畢
if index == 10{
//跳轉到分數頁面
performSegue(withIdentifier: "resultPage", sender: nil)
}else{
//未滿十題出現下一題按鈕,需按按鈕到下一題作答
nextButton.isHidden = false
}
//答錯
}else{
//錯的答案按鍵變紅色
sender.configuration?.baseBackgroundColor = .systemRed
//用UIAlertController公布正確答案與解釋
let controller = UIAlertController(title: currentAnswer, message: currentQuestion.answerDetail, preferredStyle: .alert)
//新增okAction
let okAction = UIAlertAction(title: "了解!", style: .default) { [self] action in
//判斷第十題作答完畢
if index == 10{
//跳轉到分數頁面
performSegue(withIdentifier: "resultPage", sender: nil)
}else{
//未滿十題直接進入下一題
updateUI()
}
}
controller.addAction(okAction)
present(controller, animated: true)
}
}

@IBAction func tapForNext(_ sender: UIButton) {
updateUI()
}

@IBSegueAction func showResultPage(_ coder: NSCoder) -> ResultViewController? {
let controller = ResultViewController(coder: coder)
controller?.totalScore = score
return controller
}

@IBAction func unwindToQuestion(_ unwindSegue: UIStoryboardSegue) {
_ = unwindSegue.source
viewDidLoad()

}
}

ResultViewController

import UIKit

class ResultViewController: UIViewController {
var totalScore:Int?

@IBOutlet weak var totalScoreLabel: UILabel!

override func viewDidLoad() {
super.viewDidLoad()
//隱藏<back按鍵
self.navigationItem.hidesBackButton = true
totalScoreLabel.text = String(totalScore!)
}
}

成果 Demo

後記

新年快樂🍻!!!這次順便練習了新學到的 Auto Layout 跟 Stack View,發現很難…哈哈哈!明明不太需要程式,我設定很久,最後還是沒辦法讓他在每一種機型上都正確呈現🫠尤其是iphone SE!!看來還需要再練習一陣子~

作業出處:

GitHub :

--

--

Tania
彼得潘的 Swift iOS / Flutter App 開發教室

A barista's journey transitioning into iOS development, documenting projects and learning experiences in a dedicated blog. 朝{ 咖啡師+工程師 } 努力中