#24_TableView 練習-2

Howewu
彼得潘的 Swift iOS / Flutter App 開發教室
40 min readFeb 24, 2024

記錄事項寫在提醒清單中,然後就忘記打開提醒清單

這次的練習作業來自

主要練習:資料保存、delegate 傳輸資料、純 code 寫 TableView 類型並 Autolayout、選照片的 PHPickerViewController、實現 TableView 的一些基本功能( 刪除、移動列表等等 )

因為這次是嘗試使用純 code 來寫,傳輸資料的手段大概少了一半,我對 delegate 傳輸也超不懂的,所以就在 delegate 死了不少次,不過 protocol & delegate 應該是超重要的,也只能一直重刷試著理解了🫠,現在大概能比較抓到感覺了,吧 XD,看以下

自製結構

大概 TableView 都要有一個用陣列裝著的結構,以下是參考蘋果官方教學而寫出的結構

import Foundation

struct ToDo: Equatable, Codable {
// 唯一標識符,每個待辦事項的唯一標識
let id: UUID
// 待辦事項的標題
var title: String
// 標示待辦事項是否已完成
var isComplete: Bool
// 待辦事項的截止日期
var dueDate: Date
// 可選的待辦事項備註
var notes: String?
// 可選的待辦事項圖片數據
var imageData: Data?

// 初始化函數,用於創建一個新的待辦事項實例
init(title: String, isComplete: Bool, dueDate: Date, notes: String?, imageData: Data?) {
self.id = UUID() // 生成一個新的UUID
self.title = title
self.isComplete = isComplete
self.dueDate = dueDate
self.notes = notes
self.imageData = imageData
}

// 文件管理器的默認目錄,用於存儲應用數據
static let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
// 存儲待辦事項的文件URL
static let archiveURL = documentDirectory.appendingPathComponent("toDos").appendingPathExtension("plist")



// 靜態方法,用於保存待辦事項數組到本地文件系統
static func saveToDos(_ toDos: [ToDo]) {
// 創建一個PropertyListEncoder實例用於將待辦事項數組編碼成Property List格式
let propertyListEncoder = PropertyListEncoder()
// 嘗試編碼待辦事項數組,可能會拋出錯誤,因此使用try?
let codedToDos = try? propertyListEncoder.encode(toDos)
// 將編碼後的待辦事項數據寫入到指定的URL,這裡不對文件進行保護
try? codedToDos?.write(to: archiveURL, options: .noFileProtection)
}


// 靜態方法,用於從本地文件系統加載待辦事項數組
static func loadToDos() -> [ToDo]? {
// 嘗試從archiveURL讀取數據,如果失敗則返回nil
guard let codedToDos = try? Data(contentsOf: archiveURL) else { return nil }
// 創建一個PropertyListDecoder實例用於將Property List格式的數據解碼成待辦事項數組
let propertyListDecoder = PropertyListDecoder()
// 嘗試解碼數據,如果成功則返回解碼後的待辦事項數組,如果失敗則返回nil
return try? propertyListDecoder.decode([ToDo].self, from: codedToDos)
}


// 靜態方法,用於創建一個示例待辦事項數組
static func loadSampleToDos() -> [ToDo] {
// 創建三個示例ToDo項目,用於展示或測試
let toDo1 = ToDo(title: "To-DO 1", isComplete: false, dueDate: Date(), notes: "note1", imageData: nil)
let toDo2 = ToDo(title: "To-DO 2", isComplete: false, dueDate: Date(), notes: "note2", imageData: nil)
let toDo3 = ToDo(title: "To-DO 3", isComplete: false, dueDate: Date(), notes: "note3", imageData: nil)

// 返回包含三個示例待辦事項的數組
return [toDo1, toDo2, toDo3]
}


// 靜態方法,用於比較兩個ToDo實例是否相等
static func ==(lhs: ToDo, rhs: ToDo) -> Bool {
// 比較兩個待辦事項是否有相同的id,如果是則認為它們是相同的
return lhs.id == rhs.id
}


}

遵從 Equatable 是為了讓每一個提醒事件的 UUID 能夠區分是否為同一份資料

// 靜態方法,用於比較兩個ToDo實例是否相等
static func ==(lhs: ToDo, rhs: ToDo) -> Bool {
// 比較兩個待辦事項是否有相同的id,如果是則認為它們是相同的
return lhs.id == rhs.id
}

遵從 Codable 是為了要資料儲存用,儲存圖片資料的 imageData: Data?,正好也可以使用,稍微看一下資料保存的過程

// 文件管理器的默認目錄,用於存儲應用數據
static let documentDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!

這一行程式碼的作用是獲取應用的文檔目錄的路徑。

在 iOS 中,每個應用都有一個自己的文件系統沙盒,這個沙盒分為幾個目錄,其中 documentDirectory 是用來存儲應用的用戶數據的。

這包括應用需要持久化保存的任何文件。FileManager.default 是一個預設的文件管理器實例,用於操作文件系統。.urls(for:in:) 方法用於獲取指定目錄的URL。for: .documentDirectory 表明我們要獲取的是文檔目錄,in: .userDomainMask 表示我們查詢的範圍是用戶的主目錄。.first! 是因為這個方法返回的是一個URL數組,我們通常只需要這個數組的第一個元素,即文檔目錄的路徑。

// 存儲待辦事項的文件URL
static let archiveURL = documentDirectory.appendingPathComponent("toDos").appendingPathExtension("plist")

這一行程式碼是建立在前一行的基礎上的。它使用 documentDirectory(即文檔目錄的路徑)並通過 appendingPathComponent("toDos") 添加一個新的路徑組件 “toDos”,這通常代表一個文件或文件夾的名字。

然後,使用 appendingPathExtension("plist") 為這個文件名添加一個擴展名“plist”,這是一種用於存儲序列化數據的文件格式,在這裡用於保存待辦事項的數據。

因此,archiveURL 表示了一個位於應用的文檔目錄下,名為“toDos.plist”的文件的完整路徑,這個文件用於存儲待辦事項數據。

總的來說,這兩行代碼是用來定義應用數據存儲的位置的:documentDirectory 定義了存儲數據的目錄,archiveURL 定義了具體的存儲文件的路徑和格式。

只能說是存在一個相當神秘的地方

儲存

// 靜態方法,用於保存待辦事項數組到本地文件系統
static func saveToDos(_ toDos: [ToDo]) {
// 創建一個PropertyListEncoder實例用於將待辦事項數組編碼成Property List格式
let propertyListEncoder = PropertyListEncoder()
// 嘗試編碼待辦事項數組,可能會拋出錯誤,因此使用try?
let codedToDos = try? propertyListEncoder.encode(toDos)
// 將編碼後的待辦事項數據寫入到指定的URL,這裡不對文件進行保護
try? codedToDos?.write(to: archiveURL, options: .noFileProtection)
}

這段程式碼提供了一個靜態方法 saveToDos,用於將待辦事項數組保存到本地文件系統。

static func saveToDos(_ toDos: [ToDo]) {
// 創建一個PropertyListEncoder實例用於將待辦事項數組編碼成Property List格式
let propertyListEncoder = PropertyListEncoder()

這一行初始化了一個 PropertyListEncoder 的實例。PropertyListEncoder 是一種編碼器,用於將符合 Encodable 協定的數據類型(如自定義的數據類型 ToDo)轉換成 Property List(plist)格式。Plist 是一種用於存儲序列化的數據的文件格式,常用於 iOS 應用中存儲輕量級的數據。

// 嘗試編碼待辦事項數組,可能會拋出錯誤,因此使用try?
let codedToDos = try? propertyListEncoder.encode(toDos)

這一行試圖將 toDos 數組編碼成 plist 格式。因為編碼過程可能會失敗(例如,如果 ToDo 中的某些數據不符合編碼標準),所以這裡使用 try? 來進行嘗試編碼,並將結果存儲在 codedToDos 中。

如果編碼成功,codedToDos 將包含一個非 nil 的數據對象;如果編碼失敗,codedToDos 將是 nil。

// 將編碼後的待辦事項數據寫入到指定的URL,這裡不對文件進行保護
try? codedToDos?.write(to: archiveURL, options: .noFileProtection)
}

最後,這一行將編碼後的數據(如果編碼成功的話)寫入到之前定義的 archiveURL 指向的文件中。write(to:options:) 是一個將數據寫入文件的方法,這裡也使用了 try?,因為文件寫入操作可能會失敗(例如,如果沒有寫入權限或磁盤空間不足)。options: .noFileProtection 表示在寫入文件時不添加額外的文件保護,這意味著文件可以在設備被鎖定時被訪問。

總的來說,saveToDos 方法將待辦事項的數組編碼成 plist 格式並保存到本地文件系統中,這樣即使應用關閉後,這些數據也能被保留下來。

讀取

// 靜態方法,用於從本地文件系統加載待辦事項數組
static func loadToDos() -> [ToDo]? {
// 嘗試從archiveURL讀取數據,如果失敗則返回nil
guard let codedToDos = try? Data(contentsOf: archiveURL) else { return nil }
// 創建一個PropertyListDecoder實例用於將Property List格式的數據解碼成待辦事項數組
let propertyListDecoder = PropertyListDecoder()
// 嘗試解碼數據,如果成功則返回解碼後的待辦事項數組,如果失敗則返回nil
return try? propertyListDecoder.decode([ToDo].self, from: codedToDos)
}
static func loadToDos() -> [ToDo]? {
// 嘗試從archiveURL讀取數據,如果失敗則返回nil
guard let codedToDos = try? Data(contentsOf: archiveURL) else { return nil }

這一行代碼嘗試從 archiveURL 指定的位置讀取數據。Data(contentsOf:) 是一個初始化器,用於從指定的 URL 加載數據。這裡使用 try? 表示如果讀取過程中發生任何錯誤(例如文件不存在或者讀取權限問題),則不會拋出錯誤,而是讓 codedToDos 成為 nil。使用 guard 語句是為了確保如果無法從文件系統中讀取數據,就直接返回 nil,這樣可以提前退出方法。

// 創建一個PropertyListDecoder實例用於將Property List格式的數據解碼成待辦事項數組
let propertyListDecoder = PropertyListDecoder()

這行代碼創建了一個 PropertyListDecoder 的實例。PropertyListDecoder 是用於將 Property List 格式的數據解碼(轉換)回原來的數據類型(在這裡是 [ToDo],即待辦事項的數組)的解碼器。

// 嘗試解碼數據,如果成功則返回解碼後的待辦事項數組,如果失敗則返回nil
return try? propertyListDecoder.decode([ToDo].self, from: codedToDos)
}

最後,這行代碼嘗試將先前讀取的數據 codedToDos 解碼成 [ToDo] 數組。同樣,這裡使用 try? 是為了處理任何可能的解碼錯誤:如果解碼過程成功,則返回解碼後的數組;如果解碼失敗(例如,如果數據格式不正確),則返回 nil。這樣做確保了方法的返回類型是可選的 [ToDo]?,這意味著它可以返回一個待辦事項數組或者在無法讀取或解碼數據時返回 nil。

總的來說,loadToDos 方法從應用的文件系統中讀取並解碼存儲的待辦事項數據,並將這些數據以 [ToDo] 數組的形式返回,如果在讀取或解碼過程中遇到問題,則返回 nil。

TableView 主頁

import UIKit

class ToDoTableViewController: UITableViewController {

// 用於儲存待辦事項的陣列
var toDos = [ToDo]()

// 視圖加載後的初始化工作
override func viewDidLoad() {
super.viewDidLoad()

// 嘗試從持久化存儲加載待辦事項,如果沒有則加載示例待辦事項
if let savedToDos = ToDo.loadToDos() {
toDos = savedToDos
} else {
toDos = ToDo.loadSampleToDos()
}

// 設定導航條
navigationSetting()

// 註冊自定義待辦事項單元格
tableView.register(ToDoTableViewCell.self, forCellReuseIdentifier: ToDoTableViewCell.reuseIdentifier)
}


// MARK: - Table view data source
// 設定表格只有一個區段
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}

// 設定表格的行數等於待辦事項的數量
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return toDos.count
}

// 配置每一行的單元格
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// 嘗試重用單元格,如果失敗則觸發錯誤
guard let ToDoCell = tableView.dequeueReusableCell(withIdentifier: ToDoTableViewCell.reuseIdentifier, for: indexPath) as? ToDoTableViewCell else { fatalError("dequeueReusable failed") }

// 獲取對應行的待辦事項
let toDo = toDos[indexPath.row]

// 設定單元格標題和完成狀態
ToDoCell.titleLabel.text = toDo.title
ToDoCell.delegate = self
ToDoCell.isCompleteButton.isSelected = toDo.isComplete

// 更新完成按鈕的圖像
ToDoCell.updateCompleteButtonImage()

return ToDoCell
}

// MARK: - Editing
// 啟用表格行的編輯模式,允許刪除或重新排序
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}

// 處理表格行的編輯操作,目前支持刪除操作
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
// 當用戶試圖刪除一行時
if editingStyle == .delete {
// 從數據源中移除該待辦事項
toDos.remove(at: indexPath.row)
// 從表格視圖中移除該行
tableView.deleteRows(at: [indexPath], with: .fade)
// 更新持久化存儲中的待辦事項
ToDo.saveToDos(toDos)
}
}

// MARK: - Reordering
// 允許用戶重新排序表格行
override func tableView(_ tableView: UITableView, moveRowAt fromIndexPath: IndexPath, to: IndexPath) {
// 獲取被移動的待辦事項
let movedToDo = toDos.remove(at: fromIndexPath.row)
// 將它插入到新位置
toDos.insert(movedToDo, at: to.row)
// 更新表格視圖以反映新順序
tableView.moveRow(at: fromIndexPath, to: to)

// 更新持久化存儲中的待辦事項
ToDo.saveToDos(toDos)
}

// 啟用對所有行的重新排序功能
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return true
}

// 設定表格每一行的高度
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 50 // 固定行高為 50 點
}

// 處理表格行的選擇事件
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

// 當某一行被選擇時,創建一個詳細視圖控制器來顯示或編輯該待辦事項的詳細資料
let detailViewController = ToDoDetailTableViewController(style: .grouped)

// 設定詳細視圖控制器的代理為自己,以便於接收更新後的待辦事項
detailViewController.delegate = self

// 將被選中的待辦事項傳遞給詳細視圖控制器
detailViewController.toDo = toDos[indexPath.row]

// 將詳細視圖控制器推送到導航控制器堆棧中,實現畫面轉換
navigationController?.pushViewController(detailViewController, animated: true)
}

// MARK: - Navigation
// 設定導航條的外觀和按鈕
func navigationSetting() {

// 設置左上角的編輯按鈕,由系統提供的 editButtonItem 自動處理切換編輯模式
self.navigationItem.leftBarButtonItem = self.editButtonItem
// 設置右上角的新增按鈕,點擊後會調用 addNewNoteButtonTap 方法
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addNewNoteButtonTap))

// 設置導航欄使用大標題
navigationController?.navigationBar.prefersLargeTitles = true
// 設置大標題顯示模式為始終顯示
navigationItem.largeTitleDisplayMode = .always
// 設置導航項目的標題為“提醒清單”
navigationItem.title = "提醒清單"

}

// 當用戶點擊新增按鈕時調用的方法
@objc func addNewNoteButtonTap() {

// 創建一個新的 ToDoDetailTableViewController 來讓用戶新增待辦事項
let detailViewController = ToDoDetailTableViewController(style: .grouped)

// 設定詳細視圖控制器的代理為自己,以便於接收更新後的待辦事項
detailViewController.delegate = self

// 將詳細視圖控制器推送到導航控制器堆棧中,實現畫面轉換
navigationController?.pushViewController(detailViewController, animated: true)
}
}

// MARK: - ToDoTableViewCellDelegate
// 擴展 ToDoTableViewController 以實現 ToDoTableViewCellDelegate 協定
extension ToDoTableViewController: ToDoTableViewCellDelegate {
// 當待辦事項單元格中的勾選標記被點擊時調用
func checkmarkTapped(sender: ToDoTableViewCell) {
// 獲取發送事件的單元格對應的索引路徑
if let indexPath = tableView.indexPath(for: sender) {
// 獲取並更新待辦事項的完成狀態
var toDo = toDos[indexPath.row]
toDo.isComplete.toggle() // 切換完成狀態
toDos[indexPath.row] = toDo
// 重新加載該行,以更新顯示
tableView.reloadRows(at: [indexPath], with: .fade)
// 保存更新後的待辦事項列表
ToDo.saveToDos(toDos)
}
}
}

// MARK: - ToDoDetailTableViewControllerDelegate
// 擴展 ToDoTableViewController 以實現 ToDoDetailTableViewControllerDelegate 協定
extension ToDoTableViewController: ToDoDetailTableViewControllerDelegate {
// 待辦事項詳細資料視圖控制器保存後調用
func toDoDetailTableViewControllerDidSave(_ toDo: ToDo) {
// 檢查待辦事項是否已存在於列表中
if let index = toDos.firstIndex(of: toDo) {
// 如果已存在,則更新該待辦事項
toDos[index] = toDo
} else {
// 如果不存在,則添加到列表中
toDos.append(toDo)
}
// 保存更新後的待辦事項列表
ToDo.saveToDos(toDos)
// 重新加載表格以顯示最新的數據
tableView.reloadData()
}
}

/*
這裡的註解解釋了 ToDoTableViewController
如何處理來自待辦事項單元格(ToDoTableViewCell)的委派事件,特別是當用戶點擊勾選標記時。
它也說明了如何響應待辦事項詳細視圖控制器(ToDoDetailTableViewController)中的保存事件,
包括更新現有待辦事項或添加新待辦事項到列表中,並重新加載表格視圖來反映變化。
*/

需要記住的大概是純 code 製作時需要

// 註冊自定義待辦事項單元格
tableView.register(ToDoTableViewCell.self, forCellReuseIdentifier: ToDoTableViewCell.reuseIdentifier)

我在 SceneDelegate 裡有先生成 navigationController 當作 rootViewController

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let windowScene = (scene as? UIWindowScene) else { return }

let navigationController = UINavigationController(rootViewController: ToDoTableViewController())
window = UIWindow(windowScene: windowScene)
window?.rootViewController = navigationController
window?.makeKeyAndVisible()
}

所以有多了一些 navigation 的設定

// 設定導航條的外觀和按鈕
func navigationSetting() {

// 設置左上角的編輯按鈕,由系統提供的 editButtonItem 自動處理切換編輯模式
self.navigationItem.leftBarButtonItem = self.editButtonItem
// 設置右上角的新增按鈕,點擊後會調用 addNewNoteButtonTap 方法
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addNewNoteButtonTap))

// 設置導航欄使用大標題
navigationController?.navigationBar.prefersLargeTitles = true
// 設置大標題顯示模式為始終顯示
navigationItem.largeTitleDisplayMode = .always
// 設置導航項目的標題為“提醒清單”
navigationItem.title = "提醒清單"

}

另外煩惱很久的 delegate 寫成擴展,如果要簡單的說的話就是,因為從路徑來的整包資料只在這一頁可以讀取和儲存它,所以每個小資料修改完後要儲存要讀取都要在這裡執行,要非常知道自己真正需要傳的是什麼 🙃,這可能也是 value type 比較麻煩的地方?

// MARK: - ToDoTableViewCellDelegate
// 擴展 ToDoTableViewController 以實現 ToDoTableViewCellDelegate 協定
extension ToDoTableViewController: ToDoTableViewCellDelegate {
// 當待辦事項單元格中的勾選標記被點擊時調用
func checkmarkTapped(sender: ToDoTableViewCell) {
// 獲取發送事件的單元格對應的索引路徑
if let indexPath = tableView.indexPath(for: sender) {
// 獲取並更新待辦事項的完成狀態
var toDo = toDos[indexPath.row]
toDo.isComplete.toggle() // 切換完成狀態
toDos[indexPath.row] = toDo
// 重新加載該行,以更新顯示
tableView.reloadRows(at: [indexPath], with: .fade)
// 保存更新後的待辦事項列表
ToDo.saveToDos(toDos)
}
}
}

// MARK: - ToDoDetailTableViewControllerDelegate
// 擴展 ToDoTableViewController 以實現 ToDoDetailTableViewControllerDelegate 協定
extension ToDoTableViewController: ToDoDetailTableViewControllerDelegate {
// 待辦事項詳細資料視圖控制器保存後調用
func toDoDetailTableViewControllerDidSave(_ toDo: ToDo) {
// 檢查待辦事項是否已存在於列表中
if let index = toDos.firstIndex(of: toDo) {
// 如果已存在,則更新該待辦事項
toDos[index] = toDo
} else {
// 如果不存在,則添加到列表中
toDos.append(toDo)
}
// 保存更新後的待辦事項列表
ToDo.saveToDos(toDos)
// 重新加載表格以顯示最新的數據
tableView.reloadData()
}
}

打勾勾 (CheckMark)

流程大概是這樣

在 cell 中按下打勾按鈕
⬇︎
傳遞布林值給主頁
(找出是哪筆資料)
(改變那筆資料的那項布林值)
(刷新表格按鈕圖案轉變)
(將這筆資料再存回源頭)

很難想像就這麼一個打勾勾要繞個這樣一圈😂

先看一下 cell 的程式碼

import UIKit


// 定義一個代理協定,當完成按鈕被點擊時,通知代理對象
protocol ToDoTableViewCellDelegate: AnyObject {
func checkmarkTapped(sender: ToDoTableViewCell)
}


// 自定義的 ToDoTableViewCell 類別
class ToDoTableViewCell: UITableViewCell {

// 提供一個靜態屬性來識別重用的 cell
static var reuseIdentifier: String { "\(Self.self)" }

// 定義一個代理,用於當完成按鈕被點擊時的通知
weak var delegate: ToDoTableViewCellDelegate?

// 標題標籤和完成按鈕的宣告
let titleLabel = UILabel()
let isCompleteButton = UIButton()


// 初始化函式
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit() // 呼叫共同初始化設定
}

// 需要的解碼器初始化,用於從故事板加載
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit() // 呼叫共同初始化設定
}


// 共同的初始化函式設定 UI 元件
func commonInit() {
// 將標題標籤和完成按鈕加入到 contentView
contentView.addSubview(titleLabel)
contentView.addSubview(isCompleteButton)

// 禁用自動轉換為約束
titleLabel.translatesAutoresizingMaskIntoConstraints = false
isCompleteButton.translatesAutoresizingMaskIntoConstraints = false

// 設定標題標籤的字體和顏色
titleLabel.font = UIFont.boldSystemFont(ofSize: 18)
titleLabel.textColor = .black

// 為完成按鈕設置初始圖像和動作
isCompleteButton.setImage(UIImage(systemName: "circle"), for: .normal)
isCompleteButton.addTarget(self, action: #selector(completeButtonTapped), for: .touchUpInside)

// 啟用 Auto Layout 約束
NSLayoutConstraint.activate([
isCompleteButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 1),
isCompleteButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
isCompleteButton.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.1),
isCompleteButton.heightAnchor.constraint(equalTo: isCompleteButton.widthAnchor, multiplier: 1),

titleLabel.leadingAnchor.constraint(equalTo: isCompleteButton.trailingAnchor, constant: 20),
titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
])
}


// UITableViewCell 的子類別方法,用於設置選中狀態
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// 此處可以根據需要添加額外的設置,當 cell 的選中狀態改變時
}


// 更新完成按鈕的圖像,根據當前的選中狀態決定使用哪個圖標
func updateCompleteButtonImage() {

// 根據 isCompleteButton 的選中狀態來決定使用的圖標
let buttonStateImageName = isCompleteButton.isSelected ? "checkmark.circle.fill" : "circle"
isCompleteButton.setImage(UIImage(systemName: buttonStateImageName), for: .normal)
}


// 完成按鈕被點擊時調用的方法
@objc func completeButtonTapped(sender: UIButton) {

// 通知代理完成按鈕已被點擊,傳遞當前 cell 作為參數
delegate?.checkmarkTapped(sender: self)

}

}

首先把 delegate 的相關事項給設定好,參數類型需要思考合適的,這我也覺得很生疏😂

// 定義一個代理協定,當完成按鈕被點擊時,通知代理對象
protocol ToDoTableViewCellDelegate: AnyObject {
func checkmarkTapped(sender: ToDoTableViewCell)
}

...


// 定義一個代理,用於當完成按鈕被點擊時的通知
weak var delegate: ToDoTableViewCellDelegate?

當按下打勾勾的按鈕時會讓 tableView 主頁來做事

// 完成按鈕被點擊時調用的方法
@objc func completeButtonTapped(sender: UIButton) {

// 通知代理完成按鈕已被點擊,傳遞當前 cell 作為參數
delegate?.checkmarkTapped(sender: self)

}

主頁要做的事

// MARK: - ToDoTableViewCellDelegate
// 擴展 ToDoTableViewController 以實現 ToDoTableViewCellDelegate 協定
extension ToDoTableViewController: ToDoTableViewCellDelegate {
// 當待辦事項單元格中的勾選標記被點擊時調用
func checkmarkTapped(sender: ToDoTableViewCell) {
// 獲取發送事件的單元格對應的索引路徑
if let indexPath = tableView.indexPath(for: sender) {
// 獲取並更新待辦事項的完成狀態
var toDo = toDos[indexPath.row]
toDo.isComplete.toggle() // 切換完成狀態
toDos[indexPath.row] = toDo
// 重新加載該行,以更新顯示
tableView.reloadRows(at: [indexPath], with: .fade)
// 保存更新後的待辦事項列表
ToDo.saveToDos(toDos)
}
}
}

記得在 tableView 主頁生成 cell 時先將這個 delegate 設給 tableView,然後當按鈕按下後主頁 reloadData 時就會重新改變該 cell 的布林值狀態,也要記得在這裏設置好方法讓按鈕的圖案照著布林值的數值而改變,如以下的 ToDoCell.updateCompleteButtonImage()

// 配置每一行的單元格
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// 嘗試重用單元格,如果失敗則觸發錯誤
guard let ToDoCell = tableView.dequeueReusableCell(withIdentifier: ToDoTableViewCell.reuseIdentifier, for: indexPath) as? ToDoTableViewCell else { fatalError("dequeueReusable failed") }

// 獲取對應行的待辦事項
let toDo = toDos[indexPath.row]

// 設定單元格標題和完成狀態
ToDoCell.titleLabel.text = toDo.title
ToDoCell.delegate = self
ToDoCell.isCompleteButton.isSelected = toDo.isComplete

// 更新完成按鈕的圖像
ToDoCell.updateCompleteButtonImage()

return ToDoCell
}

整筆提醒事項(新的&修改後的)

跟上面的打勾勾是差不多的流程,只是這個是傳遞整筆提醒事項的資料

直接看當我按下儲存鍵時,也是經由 delegate 丟給主頁,讓它往該資料的路徑源頭做存取等等

在編輯頁面先確認原本是否有資料,有的話就覆蓋掉,沒有就新生成一個,然後就丟給主頁

// ToDoDetailTableViewController.swift
// 當儲存按鈕被點擊時執行的動作
@objc func saveAction() {

// 從使用者介面元件中收集待辦事項資料
let title = titleTextField.text! // 強制解包,因為我們知道在這一點上標題字段不應該是空的
let isComplete = isCompleteButton.isSelected // 檢查待辦事項是否已標記為完成
let dueDate = dueDateDatePicker.date // 從日期選擇器獲取選擇的日期
let notes = notesTextView.text // 從文字視圖中獲取備註
let noteImageData = noteImageData // 從之前保存的變量中獲取備註圖片數據

// 檢查當前是否正在編輯一個已存在的待辦事項
if toDo != nil {
// 如果是,則更新該待辦事項的資料
toDo?.title = title
toDo?.isComplete = isComplete
toDo?.dueDate = dueDate
toDo?.notes = notes
toDo?.imageData = noteImageData
} else {
// 如果不是,則創建一個新的待辦事項實例
toDo = ToDo(title: title, isComplete: isComplete, dueDate: dueDate, notes: notes, imageData: noteImageData)
}

// 通過代理回調方法將更新後的或新建的待辦事項傳遞回前一個視圖控制器
delegate?.toDoDetailTableViewControllerDidSave(toDo!)
// 返回上一頁面
navigationController?.popViewController(animated: true)
}

在主頁會先確認資料是否有相同目錄的,有的話覆蓋沒有就新增,然後再存回路徑源頭

// MARK: - ToDoDetailTableViewControllerDelegate
// 擴展 ToDoTableViewController 以實現 ToDoDetailTableViewControllerDelegate 協定
extension ToDoTableViewController: ToDoDetailTableViewControllerDelegate {
// 待辦事項詳細資料視圖控制器保存後調用
func toDoDetailTableViewControllerDidSave(_ toDo: ToDo) {
// 檢查待辦事項是否已存在於列表中
if let index = toDos.firstIndex(of: toDo) {
// 如果已存在,則更新該待辦事項
toDos[index] = toDo
} else {
// 如果不存在,則添加到列表中
toDos.append(toDo)
}
// 保存更新後的待辦事項列表
ToDo.saveToDos(toDos)
// 重新加載表格以顯示最新的數據
tableView.reloadData()
}
}

以上

--

--