iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0

今天要來實作新增與編輯鬧鐘的畫面,也就是每次點擊加號按鈕後會出現的頁面。

在這個頁面中,我們可以設定鬧鐘時間、標籤名稱、重複天數、提示聲以及是否要開啟稍後提醒

定義 Delegate

為了讓這個頁面能夠回傳結果給主畫面,我們使用 Delegate Pattern
這樣在關閉新增鬧鐘頁面後,主頁面就能根據不同操作新增、修改、刪除去更新列表。

protocol AddAlarmViewControllerDelegate: AnyObject {
    func didAddNewAlarm()
    func didUpdateAlarm()
    func didDeleteAlarm()
}

建立 AddAlarmViewController

我們先建立一個新的 UIViewController,命名為 AddAlarmViewController,並加上自己的 xib 檔案。
這個頁面會包含:

  • 上方的 DatePicker
  • 中間的 TableView
  • 下方的 刪除按鈕
    
    weak var delegate: AddAlarmViewControllerDelegate?
    var repeatDays: [Bool] = Array(repeating: false, count: 7)
    var selectedSound: String = ""
    var alarmname: String = ""
    var alarmToEdit: AlarmData?
    var snoozeEnabled: Bool = true

畫面初始化與 UI 設定

進入畫面時,我們要判斷是新增鬧鐘還是編輯鬧鐘

  • 如果是新增,就設定為當前時間。
  • 如果是編輯,就讀取既有鬧鐘的資料並顯示。
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tbvAddAlarm.delegate = self
        tbvAddAlarm.dataSource = self
        tbvAddAlarm?.register(UINib(nibName: "AddAlarmTableViewCell", bundle: nil), forCellReuseIdentifier: "AddAlarmTableViewCell")
        setUI()
        setupTextFieldDelegate()
        // 設定DatePicker的初始時間為當前時間
        if let alarm = alarmToEdit, !alarm.isInvalidated {
            // 如果是編輯鬧鐘,則載入鬧鐘資料
            repeatDays = Array(alarm.repeatDays)
            // 如果鬧鐘有設定提示聲,則載入提示聲
            selectedSound = alarm.sound
            // 如果鬧鐘有設定名稱,則載入名稱
            alarmname = alarm.name
            // 如果鬧鐘有設定稍後提醒,則載入稍後提醒狀態
            snoozeEnabled = alarm.snoozeEnabled
            // 設定DatePicker的時間為鬧鐘設定的時間
            if let date = formatStringToDate(alarm.alarmTime) {
                DatePicker.date = date
            }
        }
    }

TextField 與 NavigationBar 設定

我們讓使用者能在 TableView 的第二列輸入鬧鐘標籤名稱。
同時,設定導航列上的儲存取消按鈕,以及下方的刪除鬧鐘


    // 設定TextField的代理和監聽事件
    func setupTextFieldDelegate() {
        guard let cell = tbvAddAlarm.cellForRow(at: IndexPath(row: 0, section: 0)) as? AddAlarmTableViewCell else { return }
        cell.txfRename.delegate = self
        cell.txfRename.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
    }
    

    func setUI() {
        DatePicker.locale = Locale(identifier: "zh_TW")
        title = alarmToEdit == nil ? "加入鬧鐘" : "編輯鬧鐘"
        navigationItem.rightBarButtonItem = UIBarButtonItem(title: "儲存", style: .plain, target: self, action: #selector(doneTapped))
        navigationItem.leftBarButtonItem = UIBarButtonItem(title: "取消", style: .plain, target: self, action: #selector(cancelTapped))

        
        btnDelete.isHidden = alarmToEdit == nil
        btnDelete.setTitle("刪除鬧鐘", for: .normal)
        btnDelete.setTitleColor(.red, for: .normal)
        btnDelete.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside)
    }

各種按鈕與事件動作

這部分是使用者互動的主要邏輯:

  • doneTapped():儲存或更新鬧鐘
  • cancelTapped():取消新增/編輯
  • deleteTapped():刪除鬧鐘
  • repeatButtonTapped():前往設定頁面
  • snoozeSwitchChanged():切換稍後提醒狀態
    
    // 點擊新增鬧鐘按鈕時,會跳轉到新增鬧鐘頁面
    @objc func textFieldDidChange(_ textField: UITextField) {
            alarmname = textField.text ?? "鬧鐘"
        }
    // 點擊完成按鈕時,會關閉鍵盤並儲存鬧鐘資料
    @objc func doneTapped() {
        view.endEditing(true)
        if let alarmToEdit = alarmToEdit {
            updateAlarm(alarmToEdit)
        } else {
            saveNewAlarm()
        }
    }
    // 點擊取消按鈕時,會關閉當前視圖
    @objc func cancelTapped() {
        self.dismiss(animated: true, completion: nil)
    }
    // 點擊刪除按鈕時,會刪除當前鬧鐘並關閉視圖
    @objc func deleteTapped() {
        guard let alarm = alarmToEdit else { return }
        let realm = try! Realm()
        try! realm.write {
            realm.delete(alarm)
        }
        delegate?.didDeleteAlarm()
        self.dismiss(animated: true, completion: nil)
    }
    // 點擊重複按鈕時,會跳轉到重複設定頁面
    @objc func repeatButtonTapped() {
        let repeatVC = RepeatViewController()
        repeatVC.selectedDays = repeatDays
        repeatVC.delegate = self
        navigationController?.pushViewController(repeatVC, animated: true)
    }
    // 點擊提示聲按鈕時,會跳轉到提示聲設定頁面
    @objc func soundButtonTapped() {
            let soundVC = SoundViewController()
            soundVC.delegate = self
            soundVC.currentSelectedSound = selectedSound
            navigationController?.pushViewController(soundVC, animated: true)
        }
    // 切換稍後提醒開關時,會更新`snoozeEnabled`屬性
    @objc func snoozeSwitchChanged(_ sender: UISwitch) {
        snoozeEnabled = sender.isOn
    }

時間與資料儲存

這些小工具負責將時間格式化、轉換,讓鬧鐘時間能正確顯示與儲存。

透過 Realm 來存放鬧鐘資料。
當使用者點擊「儲存」時,會根據狀況呼叫


    // 獲取系統時間,格式為"yyyyMMddHHmmss"
    func getSystemTime() -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyyMMddHHmmss"
        return formatter.string(from: Date())
    }
    // 格式化日期為"HH:mm"的字串
    func formatDate(_ date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm"
        return formatter.string(from: date)
    }
    // 將時間字串轉換為Date物件
    func formatStringToDate(_ timeString: String) -> Date? {
        let formatter = DateFormatter()
        formatter.dateFormat = "HH:mm"
        return formatter.date(from: timeString)
    }
    // 儲存新的鬧鐘到Realm資料庫
    func saveNewAlarm() {
        let realm = try! Realm()
        let newAlarm = AlarmData(
            alarmTime: formatDate(DatePicker.date),
            creatTime: getSystemTime(),
            name: alarmname,
            repeatDays: repeatDays,
            sound: selectedSound,
            snoozeEnabled: snoozeEnabled
        )
        newAlarm.isEnabled = snoozeEnabled
        try! realm.write {
            realm.add(newAlarm)
        }
        scheduleNotification(for: newAlarm)
        delegate?.didAddNewAlarm()
        self.dismiss(animated: true, completion: nil)
    }
    // 更新已存在的鬧鐘資料
    func updateAlarm(_ alarm: AlarmData) {
        let realm = try! Realm()
        try! realm.write {
            alarm.alarmTime = formatDate(DatePicker.date)
            alarm.name = alarmname.isEmpty ? "鬧鐘" : alarmname
            alarm.repeatDays.removeAll()
            alarm.repeatDays.append(objectsIn: repeatDays)
            alarm.sound = selectedSound
            alarm.isEnabled = snoozeEnabled
            alarm.snoozeEnabled = snoozeEnabled
        }
        scheduleNotification(for: alarm)
        delegate?.didUpdateAlarm()
        self.dismiss(animated: true, completion: nil)
    }

通知排程 Notification

最後一步是設定本地通知。
當鬧鐘時間到時,系統就會依照使用者設定發出提示音。

    // 排程通知
    func scheduleNotification(for alarm: AlarmData) {
            let center = UNUserNotificationCenter.current()
            let content = UNMutableNotificationContent()
            content.title = alarm.name
            content.body = "\(alarm.alarmTime)到了!"
            content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "\(alarm.sound).mp3"))
            let dateComponents = Calendar.current.dateComponents([.hour, .minute], from: formatStringToDate(alarm.alarmTime) ?? Date())
            if alarm.repeatDays.contains(true) {
                for (index, isSelected) in alarm.repeatDays.enumerated() where isSelected {
                    var triggerDateComponents = dateComponents
                    triggerDateComponents.weekday = index + 1
                    let trigger = UNCalendarNotificationTrigger(dateMatching: triggerDateComponents, repeats: true)
                    let request = UNNotificationRequest(identifier: "\(alarm.creatTime)_\(index)", content: content, trigger: trigger)
                    center.add(request) { error in
                        if let error = error {
                            print("通知排程失敗: \(error)")
                        } else {
                            print("通知已排程: \(request.identifier)")
                        }
                    }
                }
            } else {
                let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false)
                let request = UNNotificationRequest(identifier: alarm.creatTime, content: content, trigger: trigger)
                center.add(request)
            }
        }
}

結語

今天我們讓時鐘可以新增、編輯、刪除鬧鐘,還能設定通知與重複日期。


上一篇
Day 23 時鐘 2
系列文
Swift一下就會了24
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言