馬上測驗看看,你在《哈利波特》的霍格華茲魔法學校中會被分到哪一個學院?

這次要來做分類帽App,在成為iOS App魔法師前,先來看看會被分到什麼學院吧!

成果圖:

storyboard架構:

想法:

每個問題都有4個選項,每個選項都代表著一個學院,在使用者回答所有問題後,統計最多學院的選項,就是最後一個畫面要顯示的學院名稱跟圖。

程式碼:

一開始先定義Quiz這個struct,用enum定義學院並當作Option的屬性。

因後面有用亂數抽題庫,所以有實作Equable,讓Quiz可以被比較,比較的依據就是屬性quizNumber。

struct Quiz:Equatable{
static func == (lhs: Quiz, rhs: Quiz) -> Bool {
return lhs.quizNumber == rhs.quizNumber
}
let quizNumber:Int
let question:String
let options:[Option]
}
struct Option{
let content:String
let belongTo:House
}
enum House{
case Hufflepuff
case Gryffindor
case Ravenclaw
case Slytherin
}

第一個畫面只使用一個UIButton來跳轉到QuizViewController。

在QuizViewController宣告了以下變數,用途如註解

//顯示問題的內容
@IBOutlet weak var question: UITextView!
@IBOutlet weak var option1: UITextView!
@IBOutlet weak var option2: UITextView!
@IBOutlet weak var option3: UITextView!
@IBOutlet weak var option4: UITextView!
//全部問題的Quiz陣列
var allQuizzes = [Quiz]()
//儲存亂數抽取題庫後的測驗題目
var quizzes = [Quiz]()
//使用者回答題目次數
var answerTimes = 0
//儲存每次使用者選擇的選項,也是要傳到下個畫面做統計的資料
var selectedOptions = [Option]()
//目前顯示在畫面上的問題(在quizzes的索引值)
var currentQuiz = 0

在viewDidoad將資料生成,並顯示在畫面上,有個別寫成方法。

override func viewDidLoad() {
super.viewDidLoad()
//手動生成每個題目,然後一一加到allQuizzes陣列
loadQuizzes()
//亂數抽取7題當測驗題目
quizzes = getRandomQuizzes(from: allQuizzes)
//依據currentQuiz的值取出quizzes的問題,然後將內容設定給要顯示的textView
setQuiz()
}

這次遇到最大的困難就是在亂數抽取題庫,讓我發現自己程式的流程控制很弱,程式跑的跟我想的不一樣,在getRandomQuizzes我是這樣寫的,相信應該有更好的做法。

func getRandomQuizzes(from allQuizzes:[Quiz]) -> [Quiz]{
//儲存要回傳的亂數題庫
var randomQuizzes = [Quiz]()
//用來判斷亂數抽取的題庫有沒有重複
var isRepeated = false
//如果不到7題就會一直執行迴圈
while randomQuizzes.count < 7{
if let randomQuiz = allQuizzes.randomElement(){
//如果陣列長度0,跑for each會變成無窮迴圈,所以要先加入一個問題
if randomQuizzes.count == 0{
randomQuizzes.append(randomQuiz)
}else{
//將目前亂數的題庫題目一一跟本次抽出的題目比對是不是同一題,如果是就改isRepeated的值
for quiz in randomQuizzes{
if randomQuiz == quiz{
isRepeated = true
}
}
//如果都沒重複就加到題庫
if !isRepeated{
randomQuizzes.append(randomQuiz)
}
//重置確認有無重複的Bool
isRepeated = false
}
}
}
return randomQuizzes
}

setQuiz()將題目跟選項顯示在畫面上。

func setQuiz(){
question.text = quizzes[currentQuiz].question
option1.text = quizzes[currentQuiz].options[0].content
option2.text = quizzes[currentQuiz].options[1].content
option3.text = quizzes[currentQuiz].options[2].content
option4.text = quizzes[currentQuiz].options[3].content
}

將4個選項都連到相同的IBAction,不管使用者選什麼都會觸發answer方法。

由於Quiz的Option[]屬性是陣列,選項的索引值是0~3,這邊將選項設定相對應的tag值,可以透過使用者選擇的選項tag值取出option,儲存到selectedOptions陣列。

@IBAction func answer(_ sender: UIButton) {
//用使用者選取的選項tag值得到Quiz的Option陣列的option,儲存到selectedOptions
selectedOptions.append(quizzes[currentQuiz].options[sender.tag])
//當已經回答到最後一道題目(索引值 = 陣列長度 -1),就跳轉畫面
if currentQuiz == quizzes.count - 1{
performSegue(withIdentifier: "toTheEnd", sender: nil)
}else{
//還沒回答完,就把目前題數+1,再顯示新的題目的內容
currentQuiz += 1
setQuiz()
}

}

跳轉畫面時,要一併將使用者所選擇的選項selectedOptions陣列傳到ResultViewController,這邊使用的是IBSegueAction方法來傳值,傳完值後同時將使用者這次的回答記錄全部重置。

@IBSegueAction func showResult(_ coder: NSCoder) -> ResultViewController? {
let controller = ResultViewController(coder: coder)
//傳值
controller?.selectedOptions = selectedOptions
//重置本次回答紀錄
answerTimes = 0
selectedOptions.removeAll()
currentQuiz = 0
quizzes = getRandomQuizzes(from: allQuizzes)
setQuiz()
return controller
}

在ResultViewController宣告了以下變數,用途如註解。

//顯示學院結果
@IBOutlet weak var houseName: UILabel!
//顯示學院圖片
@IBOutlet weak var housePicture: UIImageView!
//儲存上個畫面傳來的值
var selectedOptions = [Option]()
//儲存本次學院結果,隨便設定一個初始值
var yourHouse:House = .Gryffindor
override func viewDidLoad() {
super.viewDidLoad()
yourHouse = getHouseResult()
setViews(with: yourHouse)
}

依據getHouseResult結果設定給setViews來顯示畫面。

func getHouseResult() -> House{
//儲存個學院被選的次數陣列
var counts = [Int]()
//儲存備選的最大次數
var maxCount = 0
//儲存個學院的次數
var countForGryffindor = 0
var countForHufflepuff = 0
var countForRavenclaw = 0
var countForSlytherin = 0
//由於有可能會有學院同票的情況,將一樣是最大次數的學院儲存到這個陣列
var finalHouses = [House]()
//跑for each將傳來的option陣列值,一一取出後,比對belongTo的值,將相對應的學院儲存次數+1
for option in selectedOptions{
switch option.belongTo{
case .Gryffindor:
countForGryffindor += 1
case .Hufflepuff:
countForHufflepuff += 1
case .Ravenclaw:
countForRavenclaw += 1
case .Slytherin:
countForSlytherin += 1
}
}
//統計結束後,儲存到一個次數陣列
counts.append(countForGryffindor)
counts.append(countForHufflepuff)
counts.append(countForRavenclaw)
counts.append(countForSlytherin)
//跑for each,如果次數比之前的maxCount大,就更新maxCount
for count in counts{
if count > maxCount{
maxCount = count
}
}
//得到maxCount後,比對個學院次數,如果跟maxCount一樣就加到finalHouses
if countForGryffindor == maxCount{
finalHouses.append(.Gryffindor)
}
if countForSlytherin == maxCount{
finalHouses.append(.Slytherin)
}
if countForHufflepuff == maxCount{
finalHouses.append(.Hufflepuff)
}
if countForRavenclaw == maxCount{
finalHouses.append(.Ravenclaw)
}
//最後從finalHouses中隨機抽取一個學院,不負責任的給使用者
return finalHouses.randomElement()!
}
//得到結果後將結果給setViews顯示畫面
func setViews(with yourHouse:House){
switch yourHouse{
case .Gryffindor:
houseName.text = "葛萊分多"
housePicture.image = UIImage(named: "Gryffindor")
case .Hufflepuff:
houseName.text = "赫夫帕夫"
housePicture.image = UIImage(named: "Hufflepuff")
case .Ravenclaw:
houseName.text = "雷文克勞"
housePicture.image = UIImage(named: "Ravenclaw")
case .Slytherin:
houseName.text = "史萊哲林"
housePicture.image = UIImage(named: "Slytherin")
}
}

如果使用者得到最後結果是史萊哲林想要重測,按鈕設定很簡單,只要dismiss最後畫面就好,因為在上個畫面傳完值後,就把回答紀錄重置了。

@IBAction func testAgain(_ sender: UIButton) {
self.dismiss(animated: true, completion: nil)
}

參考資料:

在Quiz比對是否相同時,參考這篇。

QuizViewController的blur view圓角設定參考這篇,不過注意blur view屬性只要打cornerRadius,打layer.cornerRadius會沒反應。

亂數部分參考這篇,一定要看!

傳值參考這篇。

圖片跟題目參考這個網站。

GitHub連結:

--

--