#4 Q&A問答題|水果發音練習

Yeh
彼得潘的 Swift iOS / Flutter App 開發教室
12 min readAug 23, 2022

【iOS 從零開始寫 #3】

原本要用struct做問答題,但發現水果問答的題目與答案相同,屬性只有一個,剛好彼得最近有教到enum實作,結合宋老師的語法課程,來嘗試用enum做一個陣列,跟他裝熟一下XD

設計應用

  • Image View (顯示題目:水果圖片)
  • Slider (顯示題數進度條)
  • Label (顯示題數、開始文字、答案)
  • Button (發音、開始、下一題、秀答案)
  • enum練習建立陣列
  • 訊息視窗應用(詢問是否再玩一次)

enum建立陣列

  1. 新增一個副檔名Swift File檔案,來定義enum的成員。
QnA.Swift

2. 在Swift File,使用enum建立列舉型別QnA,以case列出20顆水果成員。
(a)將原始值型別設為字串,(b)並引入CaseInterable協定型別。

【enum NOTE】
a. 當列舉的原始值型別為String時,預設的原始值為成員的名稱,可透過rawValue屬性取得水果名稱的字串。
b. 引入CaseIterable協定,才可使用allCases屬性,這個屬性是由列舉所有實體組成的陣列。
『注意』如果列舉需要設定原始值的型別時,型別必須放在繼承語法的第一個位置,協定型別放在後面。

enum QnA:String, CaseIterable
{
case apple ,banana, blueberry, cantaloupe, cherry, coconut,
grape, kiwi, lemon, lime, mango, orange, peach, pear,
pineapple, lum, pomegranate, raspberry, strawberry,
watermelon
}

3. 回到最熟悉的ViewController,產生水果陣列。

Step1. 先宣告一個型別為[String]的水果空陣列
Step2. 以迴圈配合allCass屬性列出所有的列舉實體
Step3. 由列舉實體點出原始值將水果名稱轉為字串
Step4. 將水果字串依序丟進空陣列

//宣告水果空陣列
var fruitsArray = [String]()
//所有的水果
func allFruits()
{
//迴圈配合allCass屬性列出所有case,產生水果字串的陣列
for fruit in QnA.allCases
{
let oneFruit = fruit.rawValue
fruitsArray.append(oneFruit)
}
//測試陣列是否成功產生
print(fruitsArray)
}

— — — — 我 — — — —是 — — — — 分— — — — 隔— — — — 線 — — — —

以上由enum做的陣列,其實只要將宣告一個陣列就可以完成了喔!

//直接宣告一個陣列
let fruitsArray = ["apple", "banana", "blueberry", "cantaloupe", "cherry", "coconut", "grape", "kiwi", "lemon", "lime", "mango", "orange", "peach", ]

Swift 程式碼

先拉outlet

@IBOutlet weak var numOfQuestionSlider: UISlider!
@IBOutlet weak var numOfQuestionLabel: UILabel!
@IBOutlet weak var questionImage: UIImageView!
@IBOutlet weak var readyLabel: UILabel!
@IBOutlet weak var answerLabel: UILabel!
@IBOutlet weak var answerBackground: UIImageView!
@IBOutlet weak var decoImage: UIImageView!
@IBOutlet weak var pronounceButton: UIButton!
@IBOutlet weak var startButton: UIButton!
@IBOutlet weak var nextButton: UIButton!
@IBOutlet weak var answerButton: UIButton!

宣告顯示題目的索引值,預設為0。
題數為index+1

var index = 0

將會使用的實作放進函式中

//題目
func aQuestion()
{
//顯示題目的圖片
questionImage.image = UIImage(named: fruitsArray[index])
//當前題數
numOfQuestionSlider.value = Float(index+1)
numOfQuestionLabel.text = "\(index+1)/10"
//答案未公布前,隱藏答案
answerLabel.text = ""
//答案未公布前,隱藏發音按鈕
pronounceButton.isHidden = true
}
//發音
func pronounce()
{
let utterance = AVSpeechUtterance(string: fruitsArray[index])
//調整速度
utterance.rate = 0.4
//調整音調,範圍:0.5~2
utterance.pitchMultiplier = 1.3
let synthesizer = AVSpeechSynthesizer()
synthesizer.speak(utterance)
}

allFruit()放進viewDidLoad()中,app載入畫面時就會產生所有水果的陣列fruitsArray,同時以shuffle打亂陣列重排。

override func viewDidLoad()
{
super.viewDidLoad()
//水果陣列
allFruits()
//打亂陣列重排,取代原陣列
fruitsArray.shuffle()

questionImage.isHidden = true
answerBackground.isHidden = true
goButton.isHidden = false
nextButton.isHidden = true
answerButton.isHidden = true
pronounceButton.isHidden = true
}

建立四個按鈕的實作(一直Hide來Hide去,害我也想跟著Hide起來😂)

1. 載入app後出現start鈕,點取後隱藏(isHide),並顯示next及answer按鈕。
2. 點取answer鈕後會發音一次,並顯示speak鈕,每題的answer鈕只能點取一次(isEnable),再次發音需透過speak鈕。

//開始
@IBAction func start(_ sender: UIButton!)
{
index = 0
//打亂陣列重排,取代原陣列
fruitsArray.shuffle()
aQuestion()

questionImage.isHidden = false
readyLabel.isHidden = true
answerBackground.isHidden = false
decoImage.isHidden = true
goButton.isHidden = true
nextButton.isHidden = false
answerButton.isHidden = false
}
//下一題
@IBAction func next(_ sender: Any)
{
if index == 9
{
alertMessage()
}
else
{
index += 1
aQuestion()
}
answerButton.isEnabled = true
}
//秀答案
@IBAction func answer(_ sender: Any)
{
answerLabel.text = fruitsArray[index]
//答案公布後,顯示發音按鈕
pronounceButton.isHidden = false
answerButton.isEnabled = false
pronounce()
}
//發音
@IBAction func speak(_ sender: Any)
{
pronounce()
}

遊戲結束的訊息視窗

當10題問答題結束後,按next按鈕會出現詢問是否再玩一次的視窗,點確認後重新回到第一題。

Step1. UIAlertController產生訊息視窗
Step2. UIAlertAction產生視窗上選取的按鈕(確認、取消)
Step3. addAction將按鈕加入訊息視窗中
Step4. present切換頁面來顯示訊息視窗

func alertMessage()
{
//產生訊息視窗:詢問是否再玩一次
let alertController = UIAlertController(title: "Well Done!", message: "Try again?", preferredStyle: .alert)

//確認按鈕
//閉包取作用域外的function,要加self.
let okAction = UIAlertAction(title: "Sure", style: .default) { _
in
self.start(nil)
}

//取消按鈕
let cancelAction = UIAlertAction(title: "Cancel", style: .destructive, handler: nil)

//將按鈕加入訊息視窗
alertController.addAction(okAction)
alertController.addAction(cancelAction)

//顯示訊息視窗
present(alertController, animated: true, completion: nil)
}

【UIAlertAction NOTE】
UIAlertAction是提示框的按鈕,透過建構子產生對應的實體,三個參數為:
1. title:按鈕文字
2. style:按鈕樣式 (.default預設樣式 / .destructive警告樣式,文字顯示紅色)
3. handler:可傳入閉包實作,控制點選按鈕要做的事情

『注意』okAction就有用到handler參數,來呼叫start(_ sender: UIButton)。

此處由於沒有start按鈕的實體無法輸入引數,因此start的sender需要加驚嘆號將引數變為預先拆封的選擇值。

當由別的函式呼叫時,引數就可輸入nil使用拉。

若是沒有將引數變為選擇值就輸入nil,xcode會跳紅色警告:
’nil’ is not compatible with expected argument type ‘UIButton’

更多關於訊息視窗的應用,可以看彼得的文章 👇

使用 slider 顯示目前答題進度

1. Thumb Tint 清除顏色(clear),原本的小圓點會不見。
2. 取消勾選User Interaction Enabled,元件將只會有顯示的功能,無法觸發事件,也就是説slider只有進度條的功能,無法滑動。

App實際操作

遇到的問題

使用的功能都不太熟悉,在做enum陣列時盯著xcode很久,rawValue的型別在Int和String間反覆修改;加上訊息視窗是宋老師教表格時應用到的小功能,沒有實作到傳入閉包的做法,看了點書跟文章才完成,所以這次做完很有成就感 😆

--

--