Preview

使用 auto layout 設置聊天室畫面,並將文字內容儲存到 Airtable 中。除了輸入當天日記外,也可以點擊右上角按鈕查看過去日期的日記內容、或是下拉刷新,往上滾動查看過去所有紀錄。

Topics

I. 串接 Airtable

  1. Airtable API

參考彼得潘的 Airtable 使用教學:

*建立 Airtable 後,點擊右上角的 “ ? Help” 呼叫出工具欄,到最下面的 “ <> API documentation” ,裡面有非常詳盡的API 串接方式說明。

II. NotificationCenter 通知

當使用NotificationCenter的.addObserver方法時,我們可以設置一個觀察者(observer),以便監聽特定的通知(notification)。當該通知被發送時,觀察者將執行相應的操作。NotificationCenter是iOS中用於處理通知和事件的重要機制之一。以下是.addObserver方法的詳細用法:

.addObserver 方法的基本語法

NotificationCenter.default.addObserver(
self,
selector: #selector(methodToInvoke(_:)),
name: Notification.Name("YourNotificationName"),
object: nil
)
  • self:這是觀察者對象,通常是具有處理通知的方法的類實例。當通知被觸發時,指定的方法將在這個對象上調用。
  • selector:這是觀察者應該執行的方法,當特定通知被觸發時。這是一個標有@objc的方法,它應該接受一個Notification對象作為參數。該方法應該包含我們希望在接收到通知時執行的代碼。
  • name:這是一個通知的名稱,它是一個Notification.Name對象。觀察者將監聽這個名稱的通知。當具有相同名稱的通知被發送時,觀察者將執行相應的操作。
  • object(可選):我們可以選擇性地指定一個通知的發送者對象。如果設置為nil,則觀察者將接收來自任何對象的具有相同名稱的通知。如果設置為具體的對象,則觀察者只會接收來自該對象的通知。

示例用法

以下是一個示例,說明了如何使用.addObserver方法來監聽通知:

class MyViewController: UIViewController {

override func viewDidLoad() {
super.viewDidLoad()
// 添加通知觀察者
NotificationCenter.default.addObserver(
self,
selector: #selector(handleNotification(_:)),
name: Notification.Name("MyNotification"),
object: nil
)
}
deinit {
// 在不需要時移除通知觀察者,以避免內存泄漏
NotificationCenter.default.removeObserver(self)
}
@objc func handleNotification(_ notification: Notification) {
// 在這裡執行接收到通知時執行的代碼
if let userInfo = notification.userInfo {
let message = userInfo["message"] as? String
print("Received a notification with message: \(message ?? "")")
}
}
}

在這個示例中,我們首先在viewDidLoad方法中添加了一個通知觀察者,該觀察者將在接收到名稱為"MyNotification"的通知時調用handleNotification方法。當通知被觸發時,handleNotification方法將被調用,並執行相應的代碼。

需要注意的是,在不再需要觀察通知時,應該及時使用NotificationCenter.default.removeObserver(self)來移除觀察者,以避免內存泄漏。

III. UIRefreshControl

> 教學參考此篇文章:

當我們需要在iOS應用中實現下拉刷新功能時,UIRefreshControl 是一個有用的界面元素。它可以添加到 UITableViewUICollectionView 中,並允許用戶通過下拉操作來觸發刷新操作。以下是 UIRefreshControl 的用法、屬性和方法的簡要解釋:

用法:

  1. 初始化:要使用 UIRefreshControl,首先需要創建一個實例,然後將其添加到 UITableViewUICollectionView
  2. 設置動作:要指定下拉刷新時要執行的操作(通常是重新加載數據)可以通過設置 UIRefreshControladdTarget(_:action:for:) 方法來實現的。當下拉刷新觸發時,指定的操作將被執行。
  3. 啟用刷新:確保將 UIRefreshControl 添加到的 UITableViewUICollectionView 中的 refreshControl 屬性中。這將使 UIRefreshControl 顯示在列表的頂部。

屬性:

  1. attributedTitle: 這是一個可選的 NSAttributedString,可以用於顯示刷新控件的標題。可以使用它來向用戶提供有關刷新操作的信息。
  2. isRefreshing: 這是一個 Bool 值,指示刷新控件是否處於刷新狀態。可以使用它來檢查刷新是否正在進行中,以便在刷新完成時停止刷新。

方法:

  1. beginRefreshing: 這個方法用於手動開始刷新操作。通常情況下,不需要手動調用它,因為當用戶下拉列表時,刷新控件會自動開始刷新。
  2. endRefreshing: 這個方法用於手動停止刷新操作。當數據重新加載完成時,應調用這個方法來通知刷新控件刷新已經完成。

Steps

I. 建立 Airtable

  1. 依照 airtable 資料的 JSON 格式建立 struct
API documentation 中有非常詳細的說明
struct Diary: Codable {
var records: [Records]?
}

struct Records: Codable {
var id: String?
var fields: Fields?
}

struct Fields: Codable {
var date: String?
var mood: String?
var eat: String?
var special: String?
var plan: String?
}

II. 日記輸入頁

1. 建立三種 prototype cells

var diaryFields = Fields()



func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellCount
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
//第一種欄位:日期
if indexPath.row == 0 {
let cell = contentTableView.dequeueReusableCell(withIdentifier: "\(DateTableViewCell.self)", for: indexPath) as! DateTableViewCell
let date = Date()
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let dateString = dateFormatter.string(from: date)
cell.dateLabel.text = dateString
diaryFields.date = dateString
return cell

//第二種欄位:問題
} else if indexPath.row % 2 != 0 {

let cell = contentTableView.dequeueReusableCell(withIdentifier: "\(TopicTableViewCell.self)", for: indexPath) as! TopicTableViewCell
//要顯示問題的 row
let index = (indexPath.row - 1) / 2
//新的問題欄位出現時顯示動畫,增加聊天效果
if indexPath.row == cellCount - 1, todaysEntryCompletion == false {
cell.topicTextView.text = " "
cell.ellipsisImageView.isHidden = false
//動畫效果直接使用 xcode 15 新增的 SF symbols 的 ".addSymbolEffiect" 設置
cell.ellipsisImageView.image = UIImage(systemName: "ellipsis")
cell.ellipsisImageView.addSymbolEffect(.variableColor)
//讓動畫效果跑 1.6 秒後再更新問題文字。
Timer.scheduledTimer(withTimeInterval: 1.6, repeats: false) { _ in
DispatchQueue.main.async {
if self.todaysEntryCompletion == false {
cell.topicTextView.text = self.question[index]
//因對話框是使用 auto Layout 設置,故要刷新 auto layout 確保對話框尺寸有按照新的文字內容進行調整。
cell.topicTextView.setNeedsLayout()
cell.topicTextView.layoutIfNeeded()
cell.ellipsisImageView.isHidden = true
//當所有欄位填寫完成,則停用最下方的 textView 及 submit button。詳細 func 附在下方。
self.noResponse()
}
}
}
//不是新的問題則不需要動畫效果,直接顯示文字即可。
} else {
cell.ellipsisImageView.isHidden = true
cell.topicTextView.text = self.question[index]
noResponse()
}
return cell

//第三種欄位:回答
} else {
let cell = contentTableView.dequeueReusableCell(withIdentifier: "\(ResponseTableViewCell.self)", for: indexPath) as! ResponseTableViewCell
// 把回答內文設成一組 array,直接依照順序顯示即可。
let response = [diaryFields.mood,diaryFields.eat,diaryFields.special,diaryFields.plan]
let index = (indexPath.row/2) - 1
cell.responseTextView.text = response[index]
return cell
}
}

*若所有欄位填寫完成則禁止再回答:

func noResponse() {
if cellCount > 9 {
inputTextView.isEditable = false
submitButton.isEnabled = false
}
}

2. 檢查當日是否已輸入過日記

若是當天已有紀錄,則會直接顯示:

//aittable 每一筆紀錄都有生成獨特的 ID 編號。此 ID 會在其他地方使用到,故這裡先用 escaping 導出。
func checkTodaysEntry(completion: @escaping (String?) -> Void) {

//設置 airtable API 的串接 request:
let urlString = "https://api.airtable.com/v0/appI4Y35bwZbDskfU/Diary"
var urlRequest = URLRequest(url: URL(string: urlString)!)
urlRequest.httpMethod = "GET"
urlRequest.setValue("Bearer *****", forHTTPHeaderField: "Authorization")
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")

URLSession.shared.dataTask(with: urlRequest) { data, response, error in
let decoder = JSONDecoder()
if let data {
do {
let entries = try decoder.decode(Diary.self, from: data)
if let records = entries.records {
//使用迴圈檢查 airtable 上的每一筆紀錄, 是否已存在 "今天" 的日期。
for record in records {
//直接用 Date 型別比對的話,Date 會包含到"時間"而造成比對不正確。故這邊先轉換成 "yyyy-MM-dd" 比對日期即可。
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let date = dateFormatter.string(from: Date())
//如果 airtable 上已有 "今天" 的紀錄
if record.fields?.date == date {
//將今天的紀錄保存下來
self.diaryRecord = record
//檢查今天已填寫完成幾個欄位
self.updateCellCount()
//將此筆紀錄的 ID 保存下來
self.entryID = record.id
}
}
completion(self.entryID)
}
} catch {
print("decoder error",error)
completion(nil)
}
}
}.resume()
}

*檢查當日共有多少填寫完成的欄位

//手動設置 cellCount 並轉存資料
func updateCellCount() {
if let mood = diaryRecord?.fields?.mood {
cellCount = 4
diaryFields.mood = mood
}
if let eat = diaryRecord?.fields?.eat {
cellCount = 6
diaryFields.eat = eat
}
if let special = diaryRecord?.fields?.special {
cellCount = 8
diaryFields.special = special
}
if let plan = diaryRecord?.fields?.plan {
cellCount = 10
diaryFields.plan = plan
}
print("updateCellCount",cellCount)
}

3. 更新畫面

   fileprivate func updateUI() {
let indexPath = IndexPath(row: cellCount-1, section: 0)
contentTableView.reloadData()
//固定將 tableView scroll 到最下方,確保可以看到所有最新的對話。
contentTableView.scrollToRow(at: indexPath, at: .bottom, animated: true)
}

4. 鍵盤出現時移動輸入框(NotificationCenter)

先在 Interface builder 上設定好基本的 auto layout,除了 TextView 底部距離的 constant —— 這個 constant 會在 ViewController 中用程式碼進行設置,使得鍵盤上移時, textView 及 tableView 也會跟著上移,避免遮擋輸入框及對話內容。

> func changeConstraint:

var bottomConstraint: NSLayoutConstraint?

func changeConstraint(constant: CGFloat) {
//如果已有啟用中的 bottomConstraint,則先關閉
bottomConstraint?.isActive = false
//使用 bottomAnchor 設置 auto layout constraint
bottomConstraint = inputTextView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: constant)
//啟用新設定好的 constraint
bottomConstraint!.isActive = true
}

> 於 viewDidLoad 中設定:

override func viewDidLoad() {
super.viewDidLoad()
......
inputTextView.delegate = self

//設定正常 textView 底部距離的 constant。
changeConstraint(constant: -48)

//設置 NotificationCenter ".addObserver" 於鍵盤顯示時通知
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow), //selector 欄位填寫下方設置的 @objc func 名稱
name: UIResponder.keyboardWillShowNotification, object: nil)

//設置 NotificationCenter ".addObserver" 於鍵盤隱藏時通知
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillHide),
name: UIResponder.keyboardWillHideNotification, object: nil)

......
}

> 設置對應的 @objc func:

    @objc func keyboardWillShow(notification: NSNotification) {
//使用 notification 找出 keyboard 的高度
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
{
let constant = keyboardSize.height+16
//修改 textView 底部距的 constant 來移動對話框。
changeConstraint(constant: -constant)
//強制刷新 auto layout 佈局,將 tableView 維持在最下方
view.layoutIfNeeded()
let indexPath = IndexPath(row: cellCount-1, section: 0)
contentTableView.scrollToRow(at: indexPath, at: .top, animated: false)

}
}

//當鍵盤隱藏時再次修改 textView 底部距離的 constraint
@objc func keyboardWillHide(notification: NSNotification) {
changeConstraint(constant: -48)
}

*獲取鍵盤尺寸

let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
  • notification 是一個通知對象,它包含了有關鍵盤事件的信息。通常,當鍵盤出現或消失時,系統會發送一個通知,可以通過觀察這個通知來處理相關的事件。
  • userInfo 是包含在通知中的一個字典,其中包含了有關通知的附加信息。在這個情境下,我們正在尋找包含鍵盤尺寸信息的鍵盤相關信息。
  • [UIResponder.keyboardFrameEndUserInfoKey] 是一個常數,它代表了在 userInfo 字典中用來存儲鍵盤尺寸信息的鍵。
  • as? NSValue 是一個類型轉換,它將通知中的值(可能是 NSValue)轉換為 NSValue 類型。鍵盤尺寸信息通常是以 NSValue 的形式存儲的。
  • .cgRectValue 是一個方法調用,它將 NSValue 對象轉換為 CGRect 對象,這個 CGRect 對象代表了鍵盤的尺寸和位置。

5. 送出日記內容(POST/PUT)

@IBAction func submit(_ sender: UIButton) {

//依照目前 cellCount 數量來判定出入的文字要儲存在哪裏。
if let textContent = inputTextView.text {
switch cellCount {
case 2:
diaryFields.mood = textContent
case 4:
diaryFields.eat = textContent
case 6:
diaryFields.special = textContent
case 8:
diaryFields.plan = textContent
default:
print("diaryFields update error",cellCount,textContent)
}
}
//清空 inputTextView
inputTextView.text = nil
//新增一個 CellCount 並刷新畫面,讓回答內容出現在上方 tableView 中
cellCount += 1
updateUI()

//使用 Timer 讓回答出現後 0.4 秒再新增 CellCount 並刷新畫面(開始出現動畫) >> 呈現一種 "送出對話後,對方看過才回覆" 的聊天效果。
Timer.scheduledTimer(withTimeInterval: 0.4, repeats: false) { _ in
//如果 httpMethod 是 POST (當天第一次送出紀錄),則 cellCount 直接加一即可
if self.httpMethod == "POST" {
self.cellCount += 1
//如果不是第一次送出紀錄,則 httpMethod 會是 PUT,cellCount 會受到前面提過的func updateCellCount() 影響,故需要加二維持正常數量。
} else {
self.cellCount += 2
}
self.updateUI()
}

//檢查當天是否有紀錄,如果有則 httpMethod 為 "PUT"(修改)、沒有的話則為 "POST"(新增)。
checkTodaysEntry { id in
if let id {
self.httpMethod = "PUT"
} else {
self.httpMethod = "POST"
}
//確認好 httpMethod 後開始上傳資料。詳細 func updateEntry() 見下一節。
self.updateEntry()
}

}

6. 更新 Airtable 資料

func updateEntry() {
//設置要上傳的資料結構
var entryToUpdate = Diary()
var record = Records(fields: diaryFields)
entryToUpdate.records = [record]
//如果有 entryID,表示 airtable 上已有當天的紀錄,則需使用此 ID 來達成 PUT(確保修改到正確資料)
if let entryID {
entryToUpdate.records?[0].id = entryID
}

let urlString = "https://api.airtable.com/v0/appI4Y35bwZbDskfU/Diary"
var urlRequest = URLRequest(url: URL(string: urlString)!)
//依照上一節的 checkTodaysEntry() 判定 httpMethod 為 "POST" 或是 "PUT"
urlRequest.httpMethod = httpMethod
urlRequest.setValue("Bearer *****", forHTTPHeaderField: "Authorization")
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")

//將要上傳的資料進行 JSON 編碼遍存於 httpBody 中
let encoder = JSONEncoder()
do {
let data = try encoder.encode(entryToUpdate)
urlRequest.httpBody = data
print("* update content:",String(data: urlRequest.httpBody!, encoding: .utf8) ?? "post error")
} catch {
print("encode error",error)
}

//正式開始上傳
URLSession.shared.dataTask(with: urlRequest) { data, response, error in
//檢查 http status code 以確認連接狀態
if let httpResponse = response as? HTTPURLResponse {
print("* HTTP Status Code: \(httpResponse.statusCode)")
}
//檢查回傳資料
if let data, let response = String(data: data, encoding: .utf8) {
print("check:",response)
}
}.resume()
}

7. 點擊查看歷史紀錄(單日)

直接按鈕拉 Segue 連接到日記列表 controller。

8. 下拉查看歷史紀錄(全部)

//設置 UIRefreshControl()
var refreshControl = UIRefreshControl()

override func viewDidLoad() {
super.viewDidLoad()
...
//設置此 UIRefreshControl 的 UIAction,並添加到 tableView 上。
refreshControl.addAction(UIAction(handler: { _ in
//跳轉到歷史紀錄 controller。詳細 func 內容在下方。
self.seeHistory()
}), for: .valueChanged)
contentTableView.addSubview(refreshControl)
}

*跳轉到歷史紀錄 controller:

func seeHistory() {
// 停止 UIRefreshControl
refreshControl.endRefreshing()
//將 tableView 滑動回最上方(從 navigationController 點擊 "上一頁" 返回時會看到的畫面)
let indexPath = IndexPath(row: cellCount-1, section: 0)
contentTableView.scrollToRow(at: indexPath, at: .top, animated: true)

//使用 push 跳轉 controller
guard let controller = storyboard?.instantiateViewController(withIdentifier: "\(HistoryTableViewController.self)") as? HistoryTableViewController else {return}
navigationController?.pushViewController(controller, animated: false)

}

III. 日記列表 controller

  1. 讀取 Airtable 資料(由新到舊排序)

為確保資料依照日期排序,依照 airtable API 的說明,於 urlStirng 上設置 query parameters:

var allDiaryEntries: Diary?

func getHistoryData() {
//依照日期由新到舊排序的 url:
let urlString = "https://api.airtable.com/v0/appI4Y35bwZbDskfU/Diary?sort%5B0%5D%5Bfield%5D=date&sort%5B0%5D%5Bdirection%5D=desc"
var urlRequest = URLRequest(url: URL(string: urlString)!)
urlRequest.httpMethod = "GET"
urlRequest.setValue("Bearer *****", forHTTPHeaderField: "Authorization")
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")

URLSession.shared.dataTask(with: urlRequest) { data, response, error in
let decoder = JSONDecoder()
if let data {
do {
//將取得的資料存到本地的 allDiaryEntries
self.allDiaryEntries = try decoder.decode(Diary.self, from: data)
DispatchQueue.main.async {
self.tableView.reloadData()
//因 URLSession 抓取需依定時間,故於 viewDidLoad 先設置 activity indicator view。抓取到資料後停止並隱藏。
self.loadingIndicator.stopAnimating()
self.loadingIndicator.isHidden = true
}
} catch {
print("decode error",error)
}
}
}.resume()

}

2. 設置表格畫面

var cellCount = 0

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//在 allDiaryEntries 不為 nil 的情況下更新 cellCount
if let records = allDiaryEntries?.records {
cellCount = records.count
}
return cellCount
}


override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "DateListCell", for: indexPath)
//列出每一筆日記紀錄的日期
if let records = allDiaryEntries?.records {
cell.textLabel?.text = records[indexPath.row].fields?.date
}
return cell
}

3. 查看當天日記內容

//設置 segueAction,使點選該 cell 後,跳到 歷史日記內容 Controller 顯示當天的日記紀錄
@IBSegueAction func seeOneDayDiary(_ coder: NSCoder) -> HistoryTableViewController? {
let controller = HistoryTableViewController(coder: coder)
//因 歷史日記內容 Controller 的資料是 "由舊到新" 排序,與本 controller 資料相反,故需重新計算抓取資料的 indexPath.row 並傳值。
if let index = tableView.indexPathForSelectedRow?.row {
if cellCount > 0 {
//將計算後的 index 存入歷史日記內容 Controller 的變數 "oneDayDiaryIndex"
controller?.oneDayDiaryIndex = cellCount - index - 1
}
}
return controller
}

IV. 歷史日記內容 Controller

與 日記列表 controller 不同,歷史日記內容 Controller 跟一般對話界面相同,是按照日期由舊到新排序。故 urlString 的 query parameter direction 要更改為 “=asc”。

  1. 讀取 Airtable 資料(由舊到新排序)
func getHistoryData() {
//依照日期由舊到新排序的 url:
let urlString = "https://api.airtable.com/v0/appI4Y35bwZbDskfU/Diary?sort%5B0%5D%5Bfield%5D=date&sort%5B0%5D%5Bdirection%5D=asc"
var urlRequest = URLRequest(url: URL(string: urlString)!)
urlRequest.httpMethod = "GET"
urlRequest.setValue("Bearer *****", forHTTPHeaderField: "Authorization")
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")

URLSession.shared.dataTask(with: urlRequest) { data, response, error in
let decoder = JSONDecoder()
if let data {
do {
self.allDiaryEntries = try decoder.decode(Diary.self, from: data)
DispatchQueue.main.async {
self.tableView.reloadData()
self.loadingIndicator.stopAnimating()
self.loadingIndicator.isHidden = true
self.scrolltoUpdatest()
}
} catch {
print("decode error",error)
}
}
}.resume()

}

2. 設置 cell 數量 & 內容

var oneDayDiaryIndex: Int?

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//如果是從 日記列表 controller 跳轉的,則只顯示當天的日記內容 (cellCount = 10)
if let records = allDiaryEntries?.records {
if let oneDayDiaryIndex {
cellCount = 10
//如果是從 日記輸入頁 下拉進入的,則會顯示全部的歷史資料(每一筆資料有 10 row,故 cellCount = 筆數*10)
} else {
cellCount = records.count * 10
}
}
return cellCount
}



override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

var entryIndex: Int!
//若 "oneDayDiaryIndex" 不為空值,則表示有接收到來自 日記列表 controller 的傳值。故只需顯示當天的日記即可。
if let oneDayDiaryIndex {
entryIndex = oneDayDiaryIndex
//反之若 "oneDayDiaryIndex" 為空值,則需顯示全部的紀錄。cellCount = records.count * 10,故 index 會等於 cellCount/10
} else {
entryIndex = indexPath.row / 10
}

//這裡的設置跟 日記輸入頁 三種 prototype Cells 幾乎一樣,除了 index 為 indexPath.row 的餘數。
let index = indexPath.row % 10
//餘數為 0 則顯示日期
if index == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "\(DateTableViewCell.self)", for: indexPath) as! DateTableViewCell
if let entries = allDiaryEntries?.records {
cell.dateLabel.text = entries[entryIndex].fields?.date

}
return cell

//餘數為奇數則顯示問題
} else if index % 2 != 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "\(TopicTableViewCell.self)", for: indexPath) as! TopicTableViewCell
let question = ["今天還好嗎?","都吃了些什麼?","有發生什麼特別的事情嗎?","明天有什麼計畫嗎?","期待明天又是嶄新的一天!"]
let questionIndex = (index-1)/2
cell.topicTextView.text = question[questionIndex]
return cell

//餘數為偶數則顯示回答
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "\(ResponseTableViewCell.self)", for: indexPath) as! ResponseTableViewCell
//比對餘數分別顯示日記內容欄位。因為後續會將沒有內容的欄位進行隱藏,故使用 func showTextOrHide,詳細公式內容在下方。
if let entries = allDiaryEntries?.records {
if index == 2 {
showTextOrHide(field: entries[entryIndex].fields?.mood, indexPath: indexPath, cell: cell)
} else if index == 4 {
showTextOrHide(field: entries[entryIndex].fields?.eat, indexPath: indexPath, cell: cell)
} else if index == 6 {
showTextOrHide(field: entries[entryIndex].fields?.special, indexPath: indexPath, cell: cell)
} else if index == 8 {
showTextOrHide(field: entries[entryIndex].fields?.plan, indexPath: indexPath, cell: cell)
}
}
return cell
}
}

*檢查回答欄位是否為空值

//將未作答的 indexPath 存在此 array 中:
var cellToHide: [IndexPath] = []


fileprivate func showTextOrHide(field: String?, indexPath: IndexPath, cell: ResponseTableViewCell) {
//如果日記內容欄位為空值,則將此 indexPath(回答) 以及前一個 indexPath(問題) 加入 cellToHide array 中
if field == nil {
cellToHide.append(indexPath)
let previouseIndexPath = IndexPath(row: indexPath.row-1, section: 0)
cellToHide.append(previouseIndexPath)
//如果不為空直,則顯示於 textView 上
} else {
cell.responseTextView.text = field
}
}

3. 表格沒有內容時隱藏

若使用者未填寫內容,則隱藏該問題及答案。


override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
// cellToHide aray 中的 indexPath 行高設置為 0
if cellToHide.contains(indexPath) {
return 0
// 其他的則正常依照 autoLayout 設置
} else {
return UITableView.automaticDimension
}

}

4. 畫面顯示最新日期的紀錄

//畫面一開始便顯示最新日期(最底端)的紀錄,不需動畫。想查看歷史紀錄再往上滑動。
func scrolltoUpdatest() {
if let allDiaryEntries {
let indexPath = IndexPath(row: cellCount-1, section: 0)
tableView.scrollToRow(at: indexPath, at: .bottom, animated: false)
}
}

--

--