iT邦幫忙

2025 iThome 鐵人賽

DAY 13
0
生成式 AI

iOS? AI-Yes!:用 Vide Coding 加速我的 Swift 學習曲線系列 第 13

Day 13 - 【鬧鐘實戰 IV】注入靈魂:連接 Realm 數據與左滑刪除

  • 分享至 

  • xImage
  •  

昨天,我們成功地用假資料搭建了一個外觀精美的 UI。列表看起來有模有樣,但它目前的開關按了沒反應,資料都是寫死的,更沒有辦法新增或刪除鬧鐘。

  • 困惑點:如何讓畫面和資料庫「同步」?當我刪除一筆資料時,我要如何「通知」UITableView 更新畫面?當鬧鐘 Cell 裡的開關被切換時,又要如何確保資料庫裡的狀態也同步更新?還有「新增鬧鐘」頁面儲存一個新鬧鐘後,這個列表要怎麼知道並顯示最新的項目?

  • AI 協作

    • 我的 Prompt:我準備將 UITableView 和 Realm 資料庫連接起來。我有幾個問題:從 Realm 讀取所有資料的最佳實踐是什麼?當我刪除一筆 Realm 資料後,要如何最有效率地更新 UITableView?如何處理 Cell 內部元件(例如 UISwitch)的事件,並將其狀態寫回 Realm?請簡單說明且不需要給程式碼

    AI 的回答:

    從 Realm 資料庫連接 UITableView 是一個常見的 iOS 應用程式開發任務。以下是針對您所提出的問題,提供專業且簡潔的解答,這些方法都是基於最佳實踐。

    從 Realm 讀取所有資料的最佳實踐

    從 Realm 讀取資料最有效率的方式是使用 Results 物件。Results 是一個自動更新的集合,它會即時反映資料庫中的任何變動。您不需要手動重新讀取資料,因為當資料變更時,Results 會自動更新。


    刪除 Realm 資料後更新 UITableView

    當您從 Realm 中刪除一筆資料時,最有效率的 UITableView 更新方式是利用 Results 的自動更新特性,並結合 UITableViewdeleteRows(at:with:) 方法。

    1. 首先,您需要一個Results 物件來作為 UITableView 的資料來源。
    2. 當您刪除 Realm 資料時,Results 會自動偵測到變動。
    3. 此時,您可以在 UITableViewdeleteRows 方法中,傳入對應的 IndexPath,並指定一個動畫效果(例如 .fade),來刪除表格中的那一列。

    這種方法比重新載入整個表格 (tableView.reloadData()) 更加高效,因為它只更新受到影響的單元格,同時提供了更流暢的使用者體驗。


    處理 Cell 內部元件事件並寫回 Realm

    要處理 UITableViewCell 內部元件(如 UISwitch)的事件,並將其狀態寫回 Realm,最推薦的方式是使用 Delegate 模式

    1. UITableViewCell 中定義一個 Protocol:這個 Protocol 會定義一個方法,例如 switchValueChanged(for:with:),用來將變更的狀態和對應的 IndexPath 傳遞出去。
    2. UIViewController 中實現這個 Protocol:當您在 cellForRowAt 方法中配置 Cell 時,將 UIViewController 設定為 Cell 的 Delegate。
    3. UISwitchvalueChanged 事件中調用 Delegate 方法:當 UISwitch 的狀態改變時,觸發它的 valueChanged 事件,並在事件處理函數中呼叫 Delegate 方法,將新的狀態和 Cell 的 IndexPath 傳遞給 UIViewController
    4. 在 Delegate 方法中更新 RealmUIViewController 接收到 Delegate 訊息後,會根據傳入的 IndexPath 找到對應的 Realm 物件,並在**寫入交易(write transaction)**中更新該物件的屬性。

    這種方法將 UITableViewCell 的邏輯與 UIViewController 分離,遵循了單一職責原則,使程式碼更易於維護和擴展。

    • 根據 AI 的建議,我把詳細的實作步驟整理如下:
      1. 連接真實數據庫:在 MainViewController 中引入 Realm,建立 setupRealmloadAlarms 方法,並修改 DataSource 以使用真實數據。
      2. 優化 Cell 顯示邏輯:撰寫輔助函式,讓 Cell 能根據鬧鐘的重複設定和啟用狀態,顯示更豐富的資訊和外觀。
      3. 建立互動入口:設定 NavigationBar 上的「新增」與「編輯」按鈕,並建立好目標頁面 AddAlarmViewController 的空殼與溝通用的 Delegate 協議。
      4. 實現核心互動與溝通:實作鬧鐘的「左滑刪除」、「編輯模式刪除」以及 Cell 內「開關切換」的完整功能。

步驟1:連接真實數據庫

我實作了 setupRealm()loadAlarms() 方法,並在 viewDidLoad 中依序呼叫它們。loadAlarms() 會從 Realm 抓取所有 AlarmData 物件,並根據時間排序,最後通知 TableView 刷新。

檔案:MainViewController.swift

override func viewDidLoad() {
    super.viewDidLoad()
    setupRealm() // 先初始化資料庫
    setupTableView()
    loadAlarms() // 再載入資料
}

// MARK: - 初始化設定
/// 設定 Realm 資料庫
private func setupRealm() {
    do {
        realm = try Realm()
    } catch {
        print("初始化 Realm 失敗: \(error)")
    }
}

/// 從資料庫載入所有鬧鐘資料
func loadAlarms() {
    // 從 realm 抓取所有 AlarmData 物件,並依照 alarmTime 欄位升序排序
    alarms = realm.objects(AlarmData.self).sorted(byKeyPath: "alarmTime", ascending: true)
    // 通知 TableView 畫面需要全面刷新
    tbvAlarm.reloadData()
}

最後,我修改了 tableView(_:numberOfRowsInSection:),讓它回傳 alarms?.count ?? 0,也就是真實的鬧鐘數量。

在:extension MainViewController: UITableViewDataSource, UITableViewDelegate

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    if section == 0 {
        return 1
    } else {
        // 不再回傳 3,而是回傳真實的鬧鐘數量
        return alarms?.count ?? 0
    }
}

步驟二:優化 Cell 顯示邏輯

為了讓鬧鐘資訊更一目了然,我寫了兩個輔助方法:

  • generateDisplayText(for:): 根據鬧鐘設定的重複日,產生如「鬧鐘, 每個平日」或「鬧鐘, 週一、週三」這樣的智慧提示文字。
  • updateCellAppearance(_:isEnabled:): 根據鬧鐘是否啟用,將 Cell 內的文字顏色變為灰色或正常顏色。

檔案:MainViewController.swift

// MARK: - Function
// ... 在 loadAlarms() 下方加入 ...

/// 生成顯示文字(鬧鐘名稱 + 重複資訊)
func generateDisplayText(for alarm: AlarmData) -> String {
    let daysOfWeek = ["週日", "週一", "週二", "週三", "週四", "週五", "週六"]
    let repeatDaysArray = Array(alarm.repeatDays)
    let weekdaysTemplate = [false, true, true, true, true, true, false]
    let weekendTemplate = [true, false, false, false, false, false, true]
        
    let selectedDays = repeatDaysArray.enumerated()
        .filter { $0.1 }
        .map { daysOfWeek[$0.0] }
        
    // 判斷重複模式
    var repeatText = ""
    if repeatDaysArray == weekdaysTemplate {
        repeatText = "每個平日"
    } else if repeatDaysArray == weekendTemplate {
        repeatText = "每個週末"
    } else if selectedDays.count == 7 {
        repeatText = "每天"
    } else if selectedDays.count > 0 {
        repeatText = selectedDays.joined(separator: "、")
    }
        
    // 組合顯示文字
    let alarmName = alarm.name.isEmpty ? "鬧鐘" : alarm.name
        
    if repeatText.isEmpty {
        return alarmName
    } else {
        return "\(alarmName), \(repeatText)"
    }
}

/// 更新 Cell 的外觀(顏色)
func updateCellAppearance(_ cell: AlarmTableViewCell, isEnabled: Bool) {
    cell.lbTime.textColor = isEnabled ? .label : .systemGray
    cell.lbName.textColor = isEnabled ? .label : .systemGray
}

然後,在 tableView(_:cellForRowAt:) 中,我用這兩個新方法來設定 Cell 的內容,並為 UISwitch 加上了 addTarget,讓它在狀態改變時能通知我。

檔案:MainViewController.swift

// In tableView(_:cellForRowAt:)
// ...
} else {
    let cell = tableView.dequeueReusableCell(
        withIdentifier: AlarmTableViewCell.identifier, for: indexPath
    ) as! AlarmTableViewCell
    
    let alarm = alarms[indexPath.row]
    
    // 【使用新的輔助方法來優化顯示】
    cell.lbTime.text = alarm.alarmTime
    cell.lbName.text = generateDisplayText(for: alarm)
    cell.swAlarm.isOn = alarm.isEnabled
    updateCellAppearance(cell, isEnabled: alarm.isEnabled)
    
    // 為開關加上互動,我們將在後面實現它
    cell.swAlarm.tag = indexPath.row
    cell.swAlarm.addTarget(self, action: #selector(alarmSwitchChanged(_:)), for: .valueChanged)
    
    return cell
}

步驟三:建立互動入口:導航列與跳轉

一個只能看不能增刪的列表是沒有意義的。我需要加上「新增」和「編輯」按鈕。

我先快速建立了一個空的 AddAlarmViewController.swift,並在檔案頂部定義好 AddAlarmViewControllerDelegate 協議,這是未來「新增/編輯頁」與「列表頁」溝通的合約。

檔案:AddAlarmViewController.swift

import UIKit

// 用於與主頁面溝通的代理協議
protocol AddAlarmViewControllerDelegate: AnyObject {
    func didSaveAlarm()
    func didDeleteAlarm(alarm: AlarmData)
}

class AddAlarmViewController: UIViewController {
    // MARK: - IBOutlet    
    
    // MARK: - Property
    var alarmToEdit: AlarmData? // 如果是編輯模式,這裡會有值
    weak var delegate: AddAlarmViewControllerDelegate?
}

然後,回到 MainViewController.swift,我撰寫了 setupNavigationBar() 方法來設定大標題、右邊的「+」按鈕和左邊的「編輯」按鈕。並實作了 @objc 方法 addTapped()editTapped()

檔案:MainViewController.swift

// In viewDidLoad()
// ...
loadAlarms()
setupNavigationBar() // 新增此行

// MARK: - UI Setting
func setupNavigationBar() {
    title = "鬧鐘"
    navigationController?.navigationBar.prefersLargeTitles = true
    
    // 右側新增按鈕
    navigationItem.rightBarButtonItem = UIBarButtonItem(
        image: UIImage(systemName: "plus"), style: .plain, target: self, action: #selector(addTapped)
    )
    
    // 左側編輯按鈕
    navigationItem.leftBarButtonItem = UIBarButtonItem(
        title: "編輯", style: .plain, target: self, action: #selector(editTapped)
    )
}

// MARK: - IBAction
@objc func addTapped() {
    let addAlarmVC = AddAlarmViewController(nibName: "AddAlarmViewController", bundle: nil)
    addAlarmVC.delegate = self // 設定代理人為自己
    let navController = UINavigationController(rootViewController: addAlarmVC)
    present(navController, animated: true, completion: nil)
}

@objc func editTapped() {
    isEditingMode.toggle()
    tbvAlarm.setEditing(isEditingMode, animated: true)
    navigationItem.leftBarButtonItem?.title = isEditingMode ? "完成" : "編輯"
}

步驟四:實現核心互動與溝通

最後一步,就是將所有互動的邏輯串連起來。

我讓 MainViewController 遵守剛剛建立的 AddAlarmViewControllerDelegate 協議。這樣,當未來 AddAlarmViewController 呼叫 didSaveAlarm() 時,這裡的 loadAlarms() 就會被觸發,列表也就會自動刷新!

檔案:MainViewController.swift

// MARK: - AddAlarmViewController 代理
extension MainViewController: AddAlarmViewControllerDelegate {
    func didSaveAlarm() {
        // 當收到儲存成功的通知時,重新從資料庫載入資料並刷新列表
        loadAlarms()
    }
    
    func didDeleteAlarm(alarm: AlarmData) {
        // 直接呼叫我們寫好的刪除方法
        deleteAlarm(alarm)
    }
}

接著,我實作了deleteAlarm(_:)alarmSwitchChanged(_:) 方法。

檔案:MainViewController.swift

// MARK: - Function
// ... 在 updateCellAppearance(...) 下方加入 ...

/// 刪除指定的鬧鐘
func deleteAlarm(_ alarm: AlarmData) {
    do {
        try realm.write {
            realm.delete(alarm)
        }
    } catch {
        print("刪除鬧鐘失敗: \(error)")
    }
    tbvAlarm.reloadData()
}

// MARK: - IBAction
// ... 在 editTapped() 下方加入 ...

/// 鬧鐘開關切換事件
@objc func alarmSwitchChanged(_ sender: UISwitch) {
    let rowIndex = sender.tag
    let alarmToUpdate = alarms[rowIndex]
    let newStatus = sender.isOn
    
    // 更新資料庫中的開關狀態
    do {
        try realm.write {
            alarmToUpdate.isEnabled = newStatus
        }
    } catch {
        print("從主畫面更新鬧鐘開關狀態失敗: \(error)")
    }
    // 重新載入特定行以更新 UI(注意這裡是 section 1)
    let indexPathToReload = IndexPath(row: rowIndex, section: 1)
    tbvAlarm.reloadRows(at: [indexPathToReload], with: .none)
}

最後,我實作了 tableView(_:trailingSwipeActionsConfigurationForRowAt:) 來處理「左滑刪除」,並同時處理了編輯模式下的刪除。它們最終都會呼叫我寫好的 deleteAlarm(_:) 方法,在 Realm 中安全地移除該筆資料。

檔案:MainViewController.swift

// In extension MainViewController: UITableViewDataSource, UITableViewDelegate

// 設置右滑動作
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
    guard indexPath.section == 1 else { return nil }
    let deleteAction = UIContextualAction(style: .destructive, title: "刪除") { [weak self] (_, _, completionHandler) in
        guard let self = self else { return }
        let alarmToDelete = self.alarms[indexPath.row]
        self.deleteAlarm(alarmToDelete)
        completionHandler(true)
    }
    return UISwipeActionsConfiguration(actions: [deleteAction])
}

// 處理編輯模式下的刪除
func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
    if editingStyle == .delete && indexPath.section == 1 {
        let alarmToDelete = alarms[indexPath.row]
        deleteAlarm(alarmToDelete)
    }
}

// 控制只有鬧鐘列表(section 1)可以被編輯
func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
    return indexPath.section == 1
}

最終驗證

今天我們成功地將 MainViewController 與 Realm 資料庫連接,用真實的 alarms 數據取代了原本的假資料。同時,我們不僅可以透過鬧鐘開關即時更新資料庫狀態與 UI,更能透過左滑手勢編輯模式來刪除不再需要的鬧鐘。

明天,我們將深入今天建立的 AddAlarmViewController,打造一個功能完整的新增與編輯鬧鐘頁面!


上一篇
Day 12 - 【鬧鐘實戰 III】畫面施工:用 UITableView 打造列表 UI
系列文
iOS? AI-Yes!:用 Vide Coding 加速我的 Swift 學習曲線13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言