iOS| #23 | 使用TableView與Core Data實作ToDo App
在生活中總是有許多瑣碎的代辦事項等著我們去完成,有時難免會遺忘掉某些事情或者是因為某些因素導致效率低下。今天就來透過TableView實作ToDo App來避免這些問題吧!
2022/03/17 新增功能 - iOS| #26 | 使用TableView與Core Data實作ToDo App — 新增功能
技術應用
- TableView
- Unwind Segue
- Core Data
成果展示
UI畫面
Model設計
- struct ListCoreData:負責執行CoreData相關功能
- enum DataStatus :判斷狀態為Insert或是Update
enum DataStatus {case insert , update}
功能解說&程式解說
- 點擊Cell展開DatePicker
實作好TableView,這邊欄位都是固定的,所以使用Static Cells
宣告屬性負責控制UIDatePicker的顯示與否
預設為關閉的,所以為false。並且宣告為計算屬性,在值改變的時候就會呼叫更新tableView的資料。
var datePickerStatus = false
使tableView有動畫效果
tableView.performBatchUpdates{}
更新tableView資料,這句一定要加
tableView.reloadData()
值得注意的是後面我們會利用func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath)
來控制DatePicker所屬Cell的高度。
然而當我們控制DatePicker開關時需要呼叫tableView.reloadData()
才能重複觸發func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath)
,使其達到收合的功能。
實作方法:
1. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath):選擇tableViewCell的row時觸發。
2. func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath):顯示tableViewCell時觸發。
判斷選擇日期欄位時要打開/關閉DatePicker。
if indexPath.section == 2 {/* true變false;false變true */datePickerStatus.toggle()}
根據datePickerStatus控制Row的高度。
在這邊用正向表列,只有第三個Section的第二個Row需要調整高度,其餘都讓Table自己去算出Cell 的高度。
if indexPath.section == 2 ,indexPath.row == 1{if datePickerStatus {return UITableView.automaticDimension}else{return 0}}else {return UITableView.automaticDimension}
- 使用Unwind Segue回前頁
在ListTableViewController(第一頁)實作unwind method
unwindSegue.source
:代表unwindSegue的來源,在這邊為EditTableViewController,因為是從EditTableViewController(第二頁)回到ListTableViewController(第一頁)。
在EditTableViewController(第二頁)的UIButton拉Exit
選擇我們剛剛在第一頁宣告好的@IBAction
- 建立DataModel
建立專案的時候勾選Use Core Data
若沒有勾選的話也可以自己新增(如下圖),但是相關的AppDelegate、SceneDelegate內容會有些許不同,需要再補上相關程式碼。
在這邊如果忘記勾選,建議可以直接建立一個project勾選Use Core Data,然後將程式碼複製貼上即可。
可以看到Project Navigator生出了一個xcdatamodeld檔案
Add Entity
自定義Entity名稱
新增屬性(欄位名稱)與類型
選擇Entity並且建立類別
建立完成Xcode會自動幫我們生出這兩個檔案
這時可能會跳出編譯錯誤的訊息
透過上述方法會造成以上錯誤,那是因為Codegen選擇Class Definition的緣故,其實Xcode默默地在背後已經幫我們生成好Class了,剛才我們又再建立一次才會導致編譯錯誤。
將Codegen改為Manual/None錯誤就會消失
補充:關於Codegen詳細說明可以參考此文章。
- 點擊Cell完成Todo事項
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
實作以上method,點擊cell會自動觸發此程式
let todoData = todoDataList![indexPath.row]
取得該筆資料
todoData.done.toggle()
屬性done負責控制該事項是否完成,為Bool。
toggle():true變false;false變true
tableView.deselectRow(at: indexPath, animated: true)
取消選取該cell,避免視覺顯示看起來狀態為一直被選取
do {try ListCoreData.shared.updateData(todoData: todoData)} catch let error {showAlert(message: error.localizedDescription)return}
更新CoreData資料,若有錯誤會顯示在alert controller上
func showAlert(message: String ){let alert = UIAlertController(title: "錯誤", message: message, preferredStyle: .alert)let action = UIAlertAction(title: "OK", style: .default, handler: nil)alert.addAction(action)present(alert, animated: true, completion: nil)}
func showAlert(message: )說明:
let alert = UIAlertController(title: "錯誤", message: message, preferredStyle: .alert)
生成UIAlertController
title:標題
message:訊息
preferredStyle:顯示的alert樣式,.alert將會從中間跳出視窗
let action = UIAlertAction(title: "OK", style: .default, handler: nil)
生成UIAlertAction(UIAlertController的按鈕)
title:按鈕的標題
style:文字的樣式顏色
handler:點擊按鈕後要做的事
tableView.reloadData()
更新tableView
接下來要根據todoData的屬性done來改變cell的樣子
- Core Data — Insert
利用 struct ListCoreData 的 func insertData(name: , descript: , date: )新增資料
if let appDelegate = UIApplication.shared.delegate as? AppDelegate{ }
取得AppDelegate
let context = appDelegate.persistentContainer.viewContext
宣告用來操作Core Data的常數
let toDo = NSEntityDescription.insertNewObject(forEntityName: "\(ToDoData.self)", into: context) as! ToDoData
使用NSEntityDescription.insertNewObject新增一筆資料,並且轉型為ToDoData。注意:此時資料並沒有新增進去。
forEntityName:當初建立Entity的名稱。
context:上述宣告之用來操作Core Data的常數。
toDo.name = nametoDo.descript = descripttoDo.date = datetoDo.done = false
把該筆資料的屬性值填進去
do {if context.hasChanges {try context.save()}} catch let error {throw error}
因為context.save()是一個拋出函式,所以用 do-catch包起來。先確認context內容是否有變動,確認有變動再save。注意:此時資料才新增進去。
- Core Data — Update
利用 struct ListCoreData 的func updateData(todoData: )更新資料
if let appDelegate = UIApplication.shared.delegate as? AppDelegate{ }
取得AppDelegate
let context = appDelegate.persistentContainer.viewContext
宣告用來操作Core Data的常數
do {if context.hasChanges {try context.save()}} catch let error {throw error}
因為context.save()是一個拋出函式,所以用 do-catch包起來。先確認context內容是否有變動,確認有變動再save。注意:此時資料才真正更新。
- Core Data — Read
利用 struct ListCoreData 的func fetchData()讀取資料
var toDoData: [ToDoData]?
宣告一個容器來裝抓資料的結果
if let appDelegate = UIApplication.shared.delegate as? AppDelegate{ }
取得AppDelegate
let context = appDelegate.persistentContainer.viewContext
宣告用來操作Core Data的常數
if let result = try? context.fetch(ToDoData.fetchRequest()) {toDoData = result}
利用context.fetch抓取資料(根據ToDoData.fetchRequest()的請求),並把結果給toDoData。
ToDoData.fetchRequest():建立抓資料的請求。
- Core Data — Delete
利用 struct ListCoreData 的func deleteData(todoData: )刪除資料
if let appDelegate = UIApplication.shared.delegate as? AppDelegate{ }
取得AppDelegate
let context = appDelegate.persistentContainer.viewContext
宣告用來操作Core Data的常數
context.delete(todoData)
刪除該筆CoreData資料
注意:此時資料並沒有被刪除。
do {if context.hasChanges {try context.save()}} catch let error {throw error}
因為context.save()是一個拋出函式,所以用 do-catch包起來。先確認context內容是否有變動,確認有變動再save。注意:此時資料才真正被刪除。
- 客製TableViewCell的Swipe功能
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?{ }
實作此方法,可以實現向左滑的功能;若想要往右滑動可以實作:
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?
接下來取得Swipe的該筆CoreData,我們後面會使用到它
//當筆coreData的資料let todoData = self.todoDataList![indexPath.row]
後續會分為兩點說明,分別為:
- 刪除
let delete = UIContextualAction(style: .normal, title: "") { action, view, bool in以下省略}
宣告給刪除按鈕用的UIContextualAction
style:按鈕的style
title:按鈕的title(這邊不需要給,後續會用圖片表示)
do {//刪除CoreData的當筆資料try ListCoreData.shared.deleteData(todoData: todoData)//刪除儲存todo的當筆資料self.todoDataList?.remove(at: indexPath.row)//刪除tableView上顯示的當筆資料tableView.deleteRows(at: [indexPath], with: .left)} catch let error {self.showAlert(message: error.localizedDescription)}
撰寫程式對應點擊按鈕後的動作
注意:在這邊需要刪除三樣東西,分別為CoreData的當筆資料、作為tableView資料來源的當筆資料、tableView上顯示的當筆資料。
***若tableView資料來源數量與顯示的不同,則程式會直接閃退***
以下我們來做個實驗:
將刪除tableView資料來源的當筆資料程式註解掉。
程式碼閃退,並出現錯誤訊息
接下來讓我們回到正題繼續說明
bool(true)
這個method一定要執行,否則Cell在結束動作以後顯示會不正常。但對於他的功能還是沒有全面的了解,歡迎各位大神幫忙補充。
2. 編輯
let edit = UIContextualAction(style: .normal, title: "Edit") { action, view, bool inself.performSegue(withIdentifier: "clickSwipeEditToEditController", sender: todoData)bool(true)}
在這邊我們直接call performSegue 讓程式走進 @IBSegueAction func clickSwipeEditToEditController
中準備去下一頁。
值得注意的為參數sender,我們傳入在前面已經取得我們cell的該筆CoreData。
注意:在拉@IBSegueAction時要選擇有sender的,這樣我們才可以傳遞資料過去。
@IBSegueAction func clickSwipeEditToEditController(_ coder: NSCoder, sender: Any?) -> EditTableViewController? {guard let todoData = sender as? ToDoData else {return EditTableViewController(status: .insert, coder: coder) }return EditTableViewController(status: .update, todoData: todoData, coder: coder)}
先取出sender的值,若成功轉型為ToDoData就代表是update模式;若失敗的話就代表是insert新的資料(理論上不會走到這裡,因為目前只有update會進來這個@IBSegueAction)。
以下會說明關於swipe功能的設定與調整button屬性:
delete.backgroundColor = .red
edit.backgroundColor = .systemOrange
設定delete、edit的背景顏色
delete.image = UIImage(systemName: "trash")
設定delete顯示的圖片
let swipeConfig = UISwipeActionsConfiguration(actions: [delete,edit])
建立物件UISwipeActionsConfiguration
actions:滑動後要handle的動作(button)。注意:陣列的順序與顯示的位置有關係
swipeConfig.performsFirstActionWithFullSwipe = false
此參數負責控制滑動到底是否直接執行button功能
true:滑到底會執行陣列中第一個UIContextualAction的功能(此例為delete)
false:不執行滑到底的功能
參考資料:
若內容有誤煩請指教,感謝收看。