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 的自動更新特性,並結合 UITableView 的 deleteRows(at:with:) 方法。

    1. 首先,您需要一個Results 物件來作為 UITableView 的資料來源。
    2. 當您刪除 Realm 資料時,Results 會自動偵測到變動。
    3. 此時,您可以在 UITableView 的 deleteRows 方法中,傳入對應的 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. 在 UISwitch 的 valueChanged 事件中調用 Delegate 方法:當 UISwitch 的狀態改變時,觸發它的 valueChanged 事件,並在事件處理函數中呼叫 Delegate 方法,將新的狀態和 Cell 的 IndexPath 傳遞給 UIViewController。
    4. 在 Delegate 方法中更新 Realm:UIViewController 接收到 Delegate 訊息後,會根據傳入的 IndexPath 找到對應的 Realm 物件,並在**寫入交易(write transaction)**中更新該物件的屬性。

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

    • 根據 AI 的建議,我把詳細的實作步驟整理如下:
      1. 連接真實數據庫:在 MainViewController 中引入 Realm,建立 setupRealm 和 loadAlarms 方法,並修改 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
下一篇
Day 14 - 【鬧鐘實戰 V】鬧鐘的誕生:打造新增與編輯畫面
系列文
iOS? AI-Yes!:用 Vide Coding 加速我的 Swift 學習曲線 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言