#22 製作臺灣迷因測驗 App

Delegate data transfer / Local JSON parsing / Table View / UserDefaults / UIView & NSLayoutConstraint animation

Ethan
彼得潘的 Swift iOS / Flutter App 開發教室
11 min readAug 9, 2021

--

Demo

delegate 傳資料架構

情境:

當 ViewDidLoad 中的 main thread 想透過 QuestionModel 的方法 getQuestions() 抓取遠端 JSON 資料,為了不讓 main thread 因等待資料送回而凍結 UI,我們不讓方法直接 return,而是透過 protocol 和 delegate 實現「通知 main thread 資料已送達並回傳」的目的。

如此一來,main thread 就能直接回到 ViewController 處理其他 UI 事件,抓 JSON 資料的工作交給 background thread 即可。

步驟:

  1. 發明協定和其方法。
  2. ViewController 遵從協定。
  3. 在 QuestionModel 內宣告 delegate 屬性,delegate 會遵從協定。
  4. 在 ViewController 生 QuestionModel 物件 model。ViewDidLoad 內寫 model.delegate = self
  5. 定義該協定的方法

解說:

ViewDidLoad 內,model 呼叫 getQuestions() 方法。

getQuestions() 方法內,delegate 呼叫協定的 questionsRetrieved 方法以欲傳資料為參數。即可透過 questionsRetrieved 定義的內容把資料送給 ViewController 的屬性存放

Model

QuestionData.json

Question.swift

Question 內容要與 QuestionData.json 內的資料名稱完全相同。Question 遵從 Codable protocol,就可以把 json 檔 map 到 Question。

QuizModel.swift

QuizModel 內設置 delegate property、getQuestions() 方法,getQuestions() 內呼叫的 getLocalJsonFile() 會解析本地 JSON。

QuizProtocol

View

首頁點按鈕轉場、View 圓角、畫漸層背景、拉 IBOutlet 等細節就不提了。可參考我的文章:

#09 利用 4 種 Transition Style 和多種 Presentation 製作轉場動畫

#21 製作 MBTI 十六型人格測試 App

Table View Cell 內 label 四邊都有 AutoLayout constraint,此時 Xcode 不知道我們的 cell 高度是隨內容彈性變動的,所以發出 label 文字可能被 clip 的黃色警告,可忽略。

Cell 的 Identifier 設為 “ChoiceCell”,Label 的 View 的 Tag 設為 1,cellForRowAt 方法會用到。

Cell 的 Selection 設為 None,點選時才不會有顏色跑出來。

Controller

ViewController class

宣告並初始化會用到的物件和變數。

Delegate 傳資料實現

步驟:

  1. 發明 QuizProtocol 和其方法。
  2. ViewController conforms to QuizProtocol.

3. QuizModel class 宣告 delegate property,delegate 會遵從協定。

4. 設定 ViewController 為 QuizModel 物件 model 的 delegate。

5. 定義 questionsRetrieved(_ questions:[Question]) 方法。

解說:

ViewDidLoad 內,model 呼叫 getQuestions() 方法。

getQuestions() 方法內,delegate 呼叫協定的 questionsRetrieved 方法以欲傳資料為參數。即可按照 questionsRetrieved 定義的內容把資料送給 ViewController 的屬性存放

questionsRetrieved 方法裡面,又呼叫了顯示問題方法 displayQuestion(),它可以:

(1)顯示問題圖片、名稱。(2)用 reloadData() 方法顯示 tableView。

Table View 實現

  1. ViewController 遵從 UITableViewDelegate, UITableViewDataSource。

2. 設定 ViewController 為 tableView 的 datasource(與顯示方法有關)和 delegate(與互動方法有關)

tableView.delegate = self
tableView.dataSource = self

3–1. 定義 UITableViewDatasource Methods

・numberOfRowsInSection / cellForRowAt

3–2. 定義 UITableViewDelegate Methods

・didSelectRowAt

傳資料與 present 前置作業(承上)

・ViewController 的 property:

var resultDialogVC: ResultViewController?

・viewDidLoad 內:

resultDialogVC = storyboard?.instantiateViewController(identifier: "ResultVC") as? ResultViewControllerresultDialogVC?.modalPresentationStyle = .overCurrentContext

・ResultViewController 內:

var resultTitleText: String!
var feedbackText: String!
var buttonText: String!

在 ResultViewController 點選按鈕,再換頁(跳到總結頁/第一頁/下一頁)

步驟

  1. 發明 ResultViewControllerProtocol 協定和其方法 dialogDismissed()。
  2. ViewController 遵從 ResultViewControllerProtocol。
  3. ResultViewController 內宣告 delegate property,delegate 會遵從協定。
var delegate: ResultViewControllerProtocol?

4. 將 ResultViewController 之 resultDialogVC 物件的 delegate 設為 ViewController

resultDialogVC?.delegate = self

5. 呼叫 dialogDismissed 方法:

寫在 ResultViewController 的按鈕觸發方法 nextTapped 內。

淡出元件後,先用 dismiss 方法把 presented modally 的 view controller(在此為 ResultViewController)關掉。

接著 ResultViewController 的 delegate property 呼叫 dialogDismissed 方法。

6. 定義 dialogDismissed() 方法。

用 UserDefaults 保存測驗狀態

保存狀態值方法、取得已儲存狀態值方法、清除已儲存狀態值方法

import Foundationclass StateManager {

static var numCorrectKey = "NumberCorrectKey"
static var questionIndexKey = "QuestionIndexKey"
// 保存狀態值方法
static func saveState(numCorrect: Int, questionIndex: Int) {
let defaults = UserDefaults.standard
defaults.set(numCorrect, forKey: numCorrectKey)
defaults.set(questionIndex, forKey: questionIndexKey)
}
// 取得已儲存狀態值的方法
static func retrieveValue(key: String) -> Any? {
let defaults = UserDefaults.standard
return defaults.value(forKey: key)
}
// 清除已儲存狀態值的方法
static func clearState() {
let defaults = UserDefaults.standard
defaults.removeObject(forKey: numCorrectKey)
defaults.removeObject(forKey: questionIndexKey)
}
}

dialogDismissed 方法內,顯示問題後,要保存測驗狀態

dialogDismissed 方法內,顯示總結對話窗後,要清除測驗狀態

questionsRetrieved 方法內,顯示問題之前,要回復上次測驗狀態

淡入、淡出動畫/滑入、滑出動畫

dimView / resultTitleLabel / feedbackTextView 淡入

dimView 淡出

題目畫面滑入動畫

題目滑入方法、題目滑出方法

定義:

呼叫:

顯示問題方法最後面,加入題目滑入方法。

didSelectRowAt 方法的傳資料動作之前,加入題目滑出方法。

假設有個遠端 JSON 檔格式也相同

用以下函式抓題目,getQuestions() 內也呼叫此函式。

--

--