作業#28 — 唐詩三百首測驗
練習 array、struct 自訂資料型別 、使用 Codable csv 匯入資料。
作業來源:
目錄Preview Topics
1. struct
2. Codable csv
3. array.filter()Steps
1. struct 建立資料格式、匯入資料庫
2. 作答模式
3. 選擇題
4. 測驗題
5. 問答題
6. 查看全文、語音朗讀appetize.io 模擬器 其他參考資料 完整程式碼 GitHub 交作業
Preview
此次練習同時設置了選擇題及問答題,還有會評分的測驗題:
Topics
I. struct
結構(Structures)是一種用於定義和組織相關數據的重要工具。以下是一些有關 Swift 結構的基本用法和特點:
- 定義結構:使用
struct
關鍵字可以定義一個結構。結構可以包含屬性(properties)、方法(methods)和初始化器(initializers)。例如:
struct Person {
var name: String
var age: Int
func greet() {
print("Hello, my name is \(name) and I'm \(age) years old.")
}
}
2. 創建實例:可以使用結構的初始化器來創建結構的實例。例如:
let person = Person(name: "John", age: 30)
3. 屬性訪問:可以使用點運算符來訪問結構的屬性。
print(person.name) // 輸出:"John"
print(person.age) // 輸出:30
4. 結構方法:結構可以定義屬於自身的方法。可以使用實例上的點運算符來調用結構方法。
person.greet() // 輸出:"Hello, my name is John and I'm 30 years old."
5. 值類型:結構是值類型(Value Types),這意味著當你將一個結構賦值給另一個變量或常數,或者將結構傳遞給函數時,它們會被複製。每個複製的實例是獨立的,對一個實例的更改不會影響其他實例。
var person1 = Person(name: "John", age: 30)
var person2 = person1
person2.name = "Alice"
print(person1.name) // 輸出:"John"
print(person2.name) // 輸出:"Alice"
II. Codable csv
要匯入大量資料庫時,一個一個宣告會比較複雜、程式碼也會變得很長很長。這是可以直接將資料表格以 .csv 檔輸出後,在 xcode 中使用 CodableCSV 解析。匯入方式參考彼得潘的教學文章:
III. array.filter()
filter()
是 Swift 中陣列(Array)的一個常用方法,用於根據指定的條件篩選出符合條件的元素,並返回一個新的陣列。
假設我們有一個存儲了一組整數的陣列,我們可以使用 filter()
方法來過濾出大於 5 的元素:
let numbers = [2, 5, 8, 10, 4, 7]
let filteredNumbers = numbers.filter { $0 > 5 }
print(filteredNumbers) // 輸出:[8, 10, 7]
在上述範例中,我們定義了一個閉包 { $0 > 5 }
,該閉包用於判斷元素是否大於 5。filter()
方法將應用該閉包來遍歷陣列中的每個元素,並返回一個新的陣列 filteredNumbers
,其中僅包含大於 5 的元素。
我們也可以使用更複雜的判斷條件。例如,假設我們有一個存儲了一組字串的陣列,我們可以使用 filter()
方法過濾出長度大於 3 且以特定字母開頭的字串:
let words = ["apple", "banana", "cat", "dog", "elephant"]
let filteredWords = words.filter { word in
word.count > 3 && word.hasPrefix("a")
}
print(filteredWords) // 輸出:["apple"]
如果是 arrays of struct,篩選方式如下:
let person1 = Person(name: "John", age: 30)
people.append(person1)
let person2 = Person(name: “Mary”, age: 22)
people.append(person2)
let person3 = Person(name: “Lily, age: 12)
people.append(person3)
let filteredPeople = people.filter { $0.age > 25}
print(filteredPeople) // 輸出:[Person(name: "John", age: 30)]
*在 Swift 的閉包表達式中,$0
是一種特殊的語法,用於代表閉包中的第一個參數。當使用 filter
、map
、reduce
等高階函式時,您可以使用 $0
來引用閉包的第一個參數。
Steps
1. struct 建立資料格式、匯入資料庫
先建立一個新的 swift file,用以創建 struct 結構定義。
struct Poems :Codable { //要記得加上 "Codable" !!
let title: String //詩名
let poet: String //作者
let organization: String //詩體
let content: String //內文
let question: String //問題
let answer: String //答案
}
將資料以表格整理好,確認每個表格的 title 符合 struct 中自定義的property name 後,以 .csv 檔案格式匯出並加入 xcode Assets 內。
extension Poems {
static var data: [Self] {
var array = [Self]()
if let data = NSDataAsset(name: "poem300")?.data {
let decoder = CSVDecoder {
$0.headerStrategy = .firstLine
}
do {
array = try decoder.decode([Self].self, from: data)
} catch {
print(error)
}
}
return array
}
}
將 NSDataAsset(name: "")?
中改為 csv 檔案名稱。不過不知為何我最後一欄匯入一直有問題 (Xcode 顯示 “didn’t match any CSV header.”) ,乾脆多加了一欄比較沒有意義的欄位一起匯入,但不建立它的 property,就成功讀取到所有需要的資料啦。
最後在 view controller 中加上 let array名稱 = struct名稱.data
就完成啦。後續使用方法就跟 array 一樣了。
class ViewController: UIViewController {
let poemsDataSet = Poems.data
......
}
2. 作答模式
使用 Segmented Control 建立三種模式 — — 選擇、測驗、問答。
a. 選擇題:四選一,每次隨機出題( index = Int.random(in: 0...poemsDataSet.count-1)
,可以無限玩下去。
b. 測驗題:也是選擇題,每題答對得十分,完成後會出現分數。共出十題,題目不重複,故 index 設置如下:
if index < 9 {
index += 1
} else {
showTestResult()
}
c. 問答題:不會出現選項,而只有一個 “顯示答案” 按鈕讓使用者可以查看答案。這裡也是隨機出題( index = Int.random(in: 0...poemsDataSet.count-1)
。
3. 選擇題
a. 問題
直接從 [Poems] 中隨機 index 出題。
fileprivate func setTheQuestion(dataSet: [Poems] ) {
//問題
questionLabel.text = dataSet[index].question
answerLabel.text = dataSet[index].answer
......
}
b. 選項
目標為:四個選項隨機排列、且其中之一為正確答案。
故先將正確答案從 [Poems] array 中移除後(preAnswerSelection),篩選同樣字數的 [Poems] 形成新的 array (semiAnswerSelection),重新隨機排列後取前三組資料建立成最終的 array (finalAnswerSelection),再將正確答案加入後再次重新隨機排列,即可得出。
最後再使用 for (index, buttons) in choiceButtons.enumerated() { ... }
將答案依次放到選項按鈕上。
//選項
fileprivate func setTheQuestion(dataSet: [Poems] ) {
......
var preAnswerSelection = poemsDataSet
if let correctAnswerIndex = preAnswerSelection.firstIndex(where: {$0.answer == dataSet[index].answer}) {
preAnswerSelection.remove(at: correctAnswerIndex)
}
var semiAnswerSelection = preAnswerSelection.filter {
$0.lens == dataSet[index].lens
}
var finalAnswerSelection = semiAnswerSelection.shuffled().prefix(3)
finalAnswerSelection.append(dataSet[index])
finalAnswerSelection.shuffle()
for (index, buttons) in choiceButtons.enumerated() {
buttons.setTitle(finalAnswerSelection[index], .normal)
}
......
}
當使用者選擇按鈕後,畫面會出現答題結果 — — 選擇正確為 ✓,選擇錯誤則為 ✗,並呈現出正確答案。
@IBAction func selectAnswer(_ sender: UIButton) {
//顯示答題結果
answerLabel.isHidden = false
correctnessLabel.isHidden = false
//讓 ✓ or ✗ 出現在選項的對應位置
let buttonX = sender.frame.origin.x
let buttonY = sender.frame.origin.y
correctnessLabel.frame = CGRect(x: buttonX+120, y: buttonY-35, width: 240, height: 130)
// 選擇正確為 ✓,選擇錯誤則為 ✗。
if sender.titleLabel!.text == answerLabel.text {
correctnessLabel.text = correctnessSymbol[0]
} else {
correctnessLabel.text = correctnessSymbol[1]
}
//將其他選項設為不可選
for buttons in choiceButtons {
buttons.isEnabled = false
}
sender.isEnabled = true
}
c. 下一題
點選下一題,則再次重新隨機出題。
@IBAction func goToNextQuestion(_ sender: UIButton) {
if practiceSegmentedControl.selectedSegmentIndex == 0 {
index = Int.random(in: 0...poemsDataSet.count-1)
setTheQuestion(dataSet: poemsDataSet)
}
}
4. 測驗題
a. 問題
做測驗題時,需要從 [Poems] 中選擇不重複的 10 組資料,故使用 shuffled() & preflix() 來建立新的 array。要留意 preflix 之後回傳的型別只是 ArraySlice,
若要將其更改回 Array
型別,則可在前面 Array()
轉換,不然會顯示錯誤提示。
ChatGPT 的解釋:
ArraySlice
是一種與Array
類似的型別,用於表示對現有Array
片段的引用。當您對Array
使用像是prefix
、suffix
或者切片操作符[]
時,會返回一個ArraySlice
,而不是新的Array
。
ArraySlice
的行為與Array
相似,可以進行迭代、修改元素等操作,但它僅表示原始Array
片段的引用,並不擁有自己的內存空間。
fileprivate func testMode() {
index = 0
score = 0
tenQuestionSelection = Array(poemsDataSet.shuffled().prefix(10))
setTheQuestion(dataSet: tenQuestionSelection)
......
}
此外,共出十題,故有設置進度條 Progress View。Progress View 的 value 位於 0~1 之間,型別為 Float。故我們需要將 index ( Int ) 先轉匯為 Float 並除以 10.0,以在 progress view 上顯示:
fileprivate func testMode() {
......
testProgressView.progress = Float(index) / 10.0
questionNumber.text = "第 \(index+1) 題"
}
b. 選項
選項設置方式與選擇題如出一徹,除了多一個 score
變數,每次答對會加 10 分:
// 選擇正確為 ✓,選擇錯誤則為 ✗。
if sender.titleLabel!.text == answerLabel.text {
correctnessLabel.text = correctnessSymbol[0]
score += 10
} else {
correctnessLabel.text = correctnessSymbol[1]
}
c. 下一題
總共有十題,在前九題點選 “下一題” 選項時, index
會增加,並顯示下一組題目。 Progress View 的進度條也會隨之更新。
if index < 9 {
index += 1
setTheQuestion(dataSet: tenQuestionSelection)
questionNumber.text = "第 \(index+1) 題"
testProgressView.progress = Float(index)/10.0
}
當答完最後一題後,“下一題” 按鈕會消失、取而代之的是 “再玩一次” 按鈕。 此外,也會顯示答題結果以及最終得分:
else {
//隱藏 "下一題"、出現 "再玩一次"
sender.isHidden = true
playAgainButton.isHidden = false
// progress view = 100%
testProgressView.progress = 1
//顯示測驗結果
testResultView.isHidden = false
scoreLabel.text = String(score)
}
}
d. 再玩一次
套用前面出題的公式再次出題即可。
@IBAction func playAgain(_ sender: UIButton) {
sender.isHidden = true
testMode()
}
5. 問答題
a. 問題
同選擇題出題方式。
b. 顯示答案
點擊 “顯示答案” 按鈕後,顯示出 answerLabel 即可。
@IBAction func showAnswer(_ sender: Any) {
answerLabel.isHidden = false
}
c. 下一題
類選擇題按鈕動作。
@IBAction func goToNextQuestion(_ sender: UIButton) {
if practiceSegmentedControl.selectedSegmentIndex == 2 {
index = Int.random(in: 0...poemsDataSet.count-1)
setTheQuestion(dataSet: poemsDataSet)
}
}
6. 查看全文、語音朗讀
回答問題後,畫面上會出現 “查看全文” 按鈕,點擊後會顯示出詩句的原文、詩名及作者。
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
//全詩
titleTextView.text = dataSet[index].title
poetLabel.text = dataSet[index].poet
contentTextView.text = dataSet[index].content
fullPoemView.isHidden = true //等點擊 "查看全文" 按鈕才會顯示
}
@IBAction func showFullPoem(_ sender: UIButton) {
fullPoemView.isHidden = false
}
@IBAction func hideFullPoem(_ sender: Any) {
fullPoemView.isHidden = true
speaker.stopSpeaking(at: .immediate)
}
此外,也有設置一個小喇叭按鈕,點擊後將文本轉換為語音朗讀詩詞:
import AVFAudio
class ViewController: UIViewController {
let speaker = AVSpeechSynthesizer()
override func viewDidLoad() {
super.viewDidLoad()
}
func reader (string: String) {
let poemReader = AVSpeechUtterance(string: string)
poemReader.voice = AVSpeechSynthesisVoice(language: "zh-TW")
speaker.speak(poemReader)
}
@IBAction func readPoem(_ sender: Any) {
reader(string: titleTextView.text!)
reader(string: poetLabel.text!)
reader(string: contentTextView.text)
}
畫面效果:
appetize.io
來玩玩看唐詩三百首還記得多少吧~ ( 不過這個模擬器好像沒辦法播出聲音 :/ )
*appetize.io 免費帳戶一個月只有 50 分鐘,如果出現 “Streaming for this account has been temporarily disabled” 就是這個月的額度被用完啦😆 要等下個月一號才會再重新開始計算。
其他參考資料
> 背景雲紋:
<a href=”https://zh.lovepik.com/images/png-1234797.html">Moire Png vectors by Lovepik.com</a>
完整程式碼
//
// ViewController.swift
// Poem 300
//
// Created by 陳佩琪 on 2023/5/17.
//
import UIKit
import AVFAudio
class ViewController: UIViewController {
let themeBlue = UIColor(red: 36/255, green: 60/255, blue: 112/255, alpha: 1)
let themeYellow = UIColor(red: 227/255, green: 172/255, blue: 60/255, alpha: 1)
let font:UIFont? = UIFont(name: "LXGWWenKai-Regular", size: CGFloat(20))
let speaker = AVSpeechSynthesizer()
let poemsDataSet = Poems.data
var tenQuestionSelection = [Poems]()
var index = 0
@IBOutlet var practiceSegmentedControl: UISegmentedControl!
@IBOutlet var questionLabel: UILabel!
@IBOutlet var answerLabel: UILabel!
@IBOutlet var choiceButtons: [UIButton]!
@IBOutlet var functionButtons: [UIButton]!
let functionButtonText = ["下一題 ▸","查看全詩 ▸","◂ 返回","再玩一次 ▸"]
@IBOutlet var showFullPoemButton: UIButton!
@IBOutlet var nextButton: UIButton!
@IBOutlet var playAgainButton: UIButton!
@IBOutlet var correctnessLabel: UILabel!
let correctnessSymbol = ["✓","✗"]
@IBOutlet var testStatusView: UIView!
@IBOutlet var testProgressView: UIProgressView!
@IBOutlet var questionNumber: UILabel!
@IBOutlet var testResultView: UIView!
@IBOutlet var scoreLabel: UILabel!
var score = 0
//full poem
@IBOutlet var fullPoemView: UIView!
@IBOutlet var titleTextView: UITextView!
@IBOutlet var poetLabel: UILabel!
@IBOutlet var contentTextView: UITextView!
@IBOutlet var cloudPattern: UIImageView!
@IBOutlet var cloudPatternOnResult: UIImageView!
@IBOutlet var cloudPatternOnFullPoem: UIImageView!
//QA view
@IBOutlet var questionAnswerView: UIView!
fileprivate func setTheQuestion(dataSet: [Poems] ) {
//問答
questionLabel.text = dataSet[index].question
answerLabel.text = dataSet[index].answer
answerLabel.isHidden = true
correctnessLabel.isHidden = true
testResultView.isHidden = true
showFullPoemButton.isHidden = true
nextButton.isHidden = false
playAgainButton.isHidden = true
questionAnswerView.isHidden = true
for buttons in choiceButtons {
buttons.isEnabled = true
}
var preAnswerSelection = poemsDataSet
if let correctAnswerIndex = preAnswerSelection.firstIndex(where: {$0.answer == dataSet[index].answer}) {
preAnswerSelection.remove(at: correctAnswerIndex)
}
var semiAnswerSelection = preAnswerSelection.filter {
$0.lens == dataSet[index].lens
}
var finalAnswerSelection = semiAnswerSelection.shuffled().prefix(3)
finalAnswerSelection.append(dataSet[index])
finalAnswerSelection.shuffle()
for (index, buttons) in choiceButtons.enumerated() {
let attString:NSMutableAttributedString = NSMutableAttributedString(string: finalAnswerSelection[index].answer, attributes: [.font:font!])
buttons.setAttributedTitle(attString, for: .normal)
}
//全詩
titleTextView.text = dataSet[index].title
poetLabel.text = dataSet[index].poet
contentTextView.text = dataSet[index].content
fullPoemView.isHidden = true
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
//設置 segmented control 字型
let normalAttributes: [NSAttributedString.Key: Any] = [
.foregroundColor: themeBlue,
.font: UIFont(name: "LXGWWenKai-Regular", size: 16)!
]
practiceSegmentedControl.setTitleTextAttributes(normalAttributes, for: .normal)
testStatusView.isHidden = true
index = Int.random(in: 0...poemsDataSet.count-1)
setTheQuestion(dataSet: poemsDataSet)
for (index, buttons) in functionButtons.enumerated() {
let attString:NSMutableAttributedString = NSMutableAttributedString(string: functionButtonText[index], attributes: [.font:font!])
buttons.setAttributedTitle(attString, for: .normal)
}
cloudPattern.alpha = 0.08
cloudPatternOnResult.alpha = 0.08
cloudPatternOnFullPoem.alpha = 0.08
}
fileprivate func testMode() {
index = 0
score = 0
tenQuestionSelection = Array(poemsDataSet.shuffled().prefix(10))
setTheQuestion(dataSet: tenQuestionSelection)
testStatusView.isHidden = false
testProgressView.progress = Float(index)/10.0
questionNumber.text = "第 \(index+1) 題"
}
@IBAction func selectSegmentedControl(_ sender: Any) {
if practiceSegmentedControl.selectedSegmentIndex == 0 {
index = Int.random(in: 0...poemsDataSet.count-1)
setTheQuestion(dataSet: poemsDataSet)
testStatusView.isHidden = true
questionAnswerView.isHidden = true
} else if practiceSegmentedControl.selectedSegmentIndex == 1 {
questionAnswerView.isHidden = true
testMode()
} else {
index = Int.random(in: 0...poemsDataSet.count-1)
setTheQuestion(dataSet: poemsDataSet)
testStatusView.isHidden = true
questionAnswerView.isHidden = false
}
}
@IBAction func selectAnswer(_ sender: UIButton) {
answerLabel.isHidden = false
correctnessLabel.isHidden = false
showFullPoemButton.isHidden = false
let buttonX = sender.frame.origin.x
let buttonY = sender.frame.origin.y
correctnessLabel.frame = CGRect(x: buttonX+120, y: buttonY-35, width: 240, height: 130)
if sender.titleLabel!.text == answerLabel.text {
correctnessLabel.text = correctnessSymbol[0]
score += 10
} else {
correctnessLabel.text = correctnessSymbol[1]
}
for buttons in choiceButtons {
buttons.isEnabled = false
}
sender.isEnabled = true
}
@IBAction func showFullPoem(_ sender: UIButton) {
fullPoemView.isHidden = false
}
@IBAction func hideFullPoem(_ sender: Any) {
fullPoemView.isHidden = true
speaker.stopSpeaking(at: .immediate)
}
@IBAction func goToNextQuestion(_ sender: UIButton) {
if practiceSegmentedControl.selectedSegmentIndex == 0 {
index = Int.random(in: 0...poemsDataSet.count-1)
setTheQuestion(dataSet: poemsDataSet)
} else if practiceSegmentedControl.selectedSegmentIndex == 1 {
if index < 9 {
index += 1
setTheQuestion(dataSet: tenQuestionSelection)
questionNumber.text = "第 \(index+1) 題"
testProgressView.progress = Float(index)/10.0
print(index, tenQuestionSelection[index].question)
} else {
//顯示測驗結果
sender.isHidden = true
showFullPoemButton.isHidden = true
playAgainButton.isHidden = false
testProgressView.progress = 1
correctnessLabel.isHidden = true
testResultView.isHidden = false
scoreLabel.text = String(score)
}
} else if practiceSegmentedControl.selectedSegmentIndex == 2 {
index = Int.random(in: 0...poemsDataSet.count-1)
setTheQuestion(dataSet: poemsDataSet)
questionAnswerView.isHidden = false
}
}
@IBAction func playAgain(_ sender: UIButton) {
sender.isHidden = true
testMode()
}
@IBAction func showAnswer(_ sender: Any) {
answerLabel.isHidden = false
showFullPoemButton.isHidden = false
correctnessLabel.isHidden = true
}
func reader (string: String) {
let poemReader = AVSpeechUtterance(string: string)
poemReader.voice = AVSpeechSynthesisVoice(language: "zh-TW")
speaker.speak(poemReader)
}
@IBAction func readPoem(_ sender: Any) {
reader(string: titleTextView.text!)
reader(string: poetLabel.text!)
reader(string: contentTextView.text)
}
}