Episode 38 — 選擇題 App: 猜歌遊戲

Shien
彼得潘的 Swift iOS / Flutter App 開發教室
20 min readJan 21, 2022

完整操作

設定題目

新增一個file

取名字

在新的 file 裡面建立一個 structure

struct Question {var song: String //題目的歌曲

var answerIndex: Int //題目的答案

var options: [String] //題目的選項

var image: String //答案的圖片

}

設定題目

var chineseSongs = [
Question(song: "Without You", answerIndex: 2, options: ["Runaway", "Why You Gonna Lie", "Without You"], image: "withoutYou"),
Question(song: "帥到分手", answerIndex: 1, options: ["需要你的美","帥到分手","你是我的菜"], image: "帥到分手"),
Question(song: "刻在我心底的名字", answerIndex: 0, options: ["刻在我心底的名字", "魚仔", "早安! 晨之美"], image: "刻在我心底的名字"),
Question(song: "想見你想見你想見你", answerIndex: 2, options: ["東區東區", "最好的結局", "想見你想見你想見你"], image: "想見你"),
Question(song: "地球上最浪漫的一首歌", answerIndex: 0, options: ["地球上最浪漫的一首歌", "不屑", "愛的進行曲"], image: "地球上最浪漫的一首歌"),
Question(song: "還是會", answerIndex: 1, options: ["有人在等我", "還是會", "心醉心碎"], image: "還是會"),
Question(song: "怎麼了", answerIndex: 1, options: ["Room for You", "怎麼了", "終於了解自由"], image: "怎麼了"),
Question(song: "路過人間", answerIndex: 2, options: ["微加幸福", "指望", "路過人間"], image: "路過人間"),
Question(song: "光之海", answerIndex: 0, options: ["光之海", "有一種悲傷", "幸福了 然後呢"], image: "光之海"),
Question(song: "Easy Come Easy Go", answerIndex: 1, options: ["紅色高跟鞋", "Easy Come Easy Go", "達爾文"], image: "easyComeEasyGo")
]

在建立 structure 的外面設定題目,每一個 quesiton 都要有題目歌曲、答案、題目選項跟音樂圖片

製作 storyboard

到時候適時的用 isHidden 來隱藏及顯示元件

建立 IBAction

@IBOutlet weak var gameView: UIView!

@IBOutlet weak var nextButton: UIButton! //下一題

@IBOutlet weak var playButton: UIButton! //播放鍵

@IBOutlet weak var keepPlayingLabel: UILabel! //繼續播放提示

@IBOutlet weak var checkResultButton: UIButton! //看結果鍵

@IBOutlet weak var pauseButton: UIButton! //暫停鍵

@IBOutlet weak var nextLabel: UILabel! //下一首提示

@IBOutlet var optionButtons: [UIButton]! //選擇題鍵

@IBOutlet weak var resultLabel: UILabel! //答題後提示

@IBOutlet weak var songImageView: UIImageView! //答案圖片

回到原本的 controller 建立

選擇題鍵有三個,先隨便連一個按鈕建立 outlet collection 形成 array。再將其他兩個按鈕連進來。

var index = 0

先宣告一個題目數字

func setTheGame(songType: [Question], totalSongs: Int, options: [String], image: String){


music(type: songType, index: index)

for i in 0...2 {

optionButtons[i].setTitle(options[i], for: .normal)

}

setTime()
displayTime()

}

再宣告了一個顯示每一題題目的方法,先使用 music function 來呼叫歌曲,再用 for 迴圈來設定該題目歌曲的選項到三個按鈕上。利用 setTitle 的方式幫按鈕上名字。

音樂播放

import AVFoundation

輸入一個能播放音樂的資料庫

let player = AVPlayer()

建立一個播放音樂的 player

加入 mp3 檔

Copy items if needed 記得勾選

func music(type: [Question], index: Int) {

let fileURL = Bundle.main.url(forResource: type[index].song, withExtension: "mp3")

let playerItem = AVPlayerItem(url: fileURL!)
player.replaceCurrentItem(with: playerItem)

}

宣告一個儲存待播音樂的方法,這邊有一個參數傳入 [Question],可以在 forResource 那邊呼叫歌曲。

@IBAction func play(_ sender: UIButton) {

switch sender {

case pauseButton:
player.pause() //暫停播放
pauseButton.isHidden = true
playButton.isHidden = false

default:
player.play() //開始播放
playButton.isHidden = true
pauseButton.isHidden = false
}


setTime()
displayTime()

}

將播放鍵即暫停鍵連成同一個 IBAction,然後使用 switch 來判斷按的是播放鍵還是暫停鍵。

顯示下一題

@IBAction func next(_ sender: UIButton) {

nextButton.isHidden = true
nextLabel.isHidden = true
keepPlayingLabel.isHidden = true

index += 1

if isChinese {

setTheGame(songType: chineseSongs, totalSongs: chineseSongs.count, options: chineseSongs[index].options, image: chineseSongs[index].image)

} else if isEnglish {

setTheGame(songType: engilshSongs, totalSongs: engilshSongs.count, options: engilshSongs[index].options, image: engilshSongs[index].image)

}


pauseButton.isHidden = false
playButton.isHidden = true
songImageView.isHidden = true
resultLabel.text = ""
player.play()
}

按下下一題的按鈕後,index 除了加一以外,還要利用加一後的 index 來取得下一首歌的值。

歌曲時間顯示

@objc func displayTime() {


let remainTime = String(format: "%.0f", (Double(180)-player.currentTime().seconds))

remainMinuteText = String(Int(remainTime)!/60)


remainSecondText = String( Int(remainTime)! % 60)


if Int(remainSecondText)! < 10 {
remainTimeLabel.text = "0\(remainMinuteText):0\(remainSecondText)"

} else {
remainTimeLabel.text = "0\(remainMinuteText):\(remainSecondText)"

}

}

利用播放器目前的時間來顯示到數。因為我的歌都只有三分鐘所以我直接用 180 秒去剪掉播放器目前讀到的秒數。用 除以 60 獲得分鐘數、用 60 求餘數得到秒數。我這邊有歌缺點是有時候用 ! 去硬拆會是 nil ,可能還要再研究一下更好的用法。

func setTimer() {
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(displayTime) , userInfo: nil, repeats: true)

}

設定到數計時器,並選擇剛剛建立的時間顯示方法去執行。

控制音量

@IBAction func controlVolume(_ sender: UISlider) {

player.volume = sender.value

}

創建一個 slider 的 IBAction,把 slider 的值給 player 的音量即可

建立主頁面及改變題目順序

建立 storyboard

這個是玩遊戲前出現的選擇畫面,所以選擇題的 view 在他下面。

var isChinese = false
var isEnglish = false

我宣告兩個變數來判斷是中文歌曲還是英文歌曲

@IBAction func shuffleSongs(_ sender: UIButton) {

playButton.isHidden = false

switch sender {

case chineseSongsButton:

chineseSongs.shuffle() //當選到中文歌曲就開始洗牌題目
isChinese = true

setTheGame(songType: chineseSongs, totalSongs: chineseSongs.count, options: chineseSongs[index].options, image: chineseSongs[index].image)

case engilshSongsButton:
engilshSongs.shuffle() //當選到英文歌曲就開始洗牌題目

isEnglish = true

setTheGame(songType: engilshSongs, totalSongs: engilshSongs.count, options: engilshSongs[index].options, image: engilshSongs[index].image)

default:
index += 0
}

player.play()
displayTime()

gameView.isHidden = false
songTypeView.isHidden = true
pauseButton.isHidden = false

}

利用 .shuffle() 可以將矩陣裡的值洗牌

答對或答錯

var correctCount = 0
var wrongCount = 0

宣告對錯數量的變數

func addCount(answerIndex: Int, songName: String, buttonIndex: Int, imageName: String) {


if answerIndex == buttonIndex {

resultLabel.text = "Congradulations! 你答對了!"

correctCount += 1

} else {

resultLabel.text = "Oops! 這首歌是 \(songName)"

wrongCount += 1
}

songImageView.image = UIImage(named: imageName )

}

宣告一個增加對錯數目的方法。參數中,answerIndex 要取得題目的正確答案。songName 要取得播放音樂。buttonIndex 要取得選擇按鈕的代號。imageName 要取得該首歌的圖片。

@IBAction func checkAnswer(_ sender: UIButton) {

songImageView.isHidden = false

switch sender {

case optionButtons[0]:

if isChinese {

addCount(answerIndex: chineseSongs[index].answerIndex, songName: chineseSongs[index].song, buttonIndex: 0, imageName: chineseSongs[index].image)

} else if isEnglish {

addCount(answerIndex: engilshSongs[index].answerIndex, songName: engilshSongs[index].song, buttonIndex: 0, imageName: engilshSongs[index].image)

}


case optionButtons[1]:

// 依此類推

default:
//依此類推
}
.....

將三個選擇按鈕連成同一個IBAction,用選擇項目的按鈕來判斷是否正確。當按到第一個按鈕,先分辨是中文還是英文歌曲,再將該歌曲的相關屬性丟進 addCount 當參數去增加對錯數量及顯示答案圖片。

遊戲結果

製作storyboard

這個是遊戲結果頁面,我把它擺最下面。

建立 IBOutlet

@IBOutlet weak var scoreView: UIView! //顯示結果的頁面@IBOutlet weak var scoreLabel: UILabel! //顯示對錯題數@IBOutlet weak var trophyImageView: UIImageView! //顯示獲得的獎項@IBOutlet weak var descriptionLabel: UILabel! //顯示描述內容@IBOutlet weak var playAgainButton: UIButton! //再玩一次的按鈕

建立IBAction

@IBAction func checkResult(_ sender: Any) {

scoreView.isHidden = false
gameView.isHidden = true

scoreLabel.text = "答對 \(correctCount) 題, 答錯 \(wrongCount) 題"

if correctCount < 3 {

descriptionLabel.text = "你需要多聽歌!!"


} else if correctCount < 6 {

descriptionLabel.text = "還可以 有在聽歌\n 獲得銅色音符"
trophyImageView.image = UIImage(named: copperImages.randomElement()!)
addImage(image: copperImages, collection: copperCollection)

} else if correctCount < 9 {

descriptionLabel.text = "不錯嘛 很常聽歌\n 獲得銀色音符"
trophyImageView.image = UIImage(named: silverImages.randomElement()!)
addImage(image: silverImages, collection: silverCollection)

} else {

descriptionLabel.text = "猜歌大神\n 獲得金色音符"
trophyImageView.image = UIImage(named: goldenImages.randomElement()!)


addImage(image: goldenImages, collection: goldenCollection)
}
}

選擇題玩到最後一題會跳出結果按鈕,點擊按鈕即顯示結果。利用 correctCount 跟 wrongCount 來描述對錯幾題。用 if else 來判斷對的數量及顯示內容。

加入徽章

var copperImages = ["copperNote1", "copperNote2"]var goldenImages = ["goldenNote1", "goldenNote2", "goldenNote3", "goldenNote4"]var silverImages = ["silverNote1", "silverNote2", "silverNote3", "silverNote4"]

分別儲存金銀銅色音符的照片

製作 storyboard

每次玩完遊戲會發一個顏色的音符徽章,該徽章會顯示在這個頁面。

建立 IBOutlet

@IBOutlet weak var trophyView: UIView! //獎章搜集頁面@IBOutlet var goldenCollection: [UIImageView]! //金色音符框@IBOutlet var silverCollection: [UIImageView]! //銀色音符框@IBOutlet var copperCollection: [UIImageView]! //銅色音符框@IBOutlet weak var backToTypeButton: UIButton! //反會按鈕func addImage(image: [String], collection: [UIImageView]){

var num = 3

if collection == copperCollection {
num = 1

}

for index in 0...num {

if trophyImageView.image == UIImage(named: image[index]) {

collection[index].image = UIImage(named: image[index])
}

}

}

宣告一個把徽章加入圖庫的方法,參數 image 取得照片名稱的 array,collection 要取得圖框的 array。使用 for 迴圈並用 if else 判斷抽到的徽章是否等於該圖框的徽章。

--

--