今天要來實作新增與編輯鬧鐘的畫面,也就是每次點擊加號按鈕後會出現的頁面。
在這個頁面中,我們可以設定鬧鐘時間、標籤名稱、重複天數、提示聲以及是否要開啟稍後提醒。
為了讓這個頁面能夠回傳結果給主畫面,我們使用 Delegate Pattern
。
這樣在關閉新增鬧鐘頁面後,主頁面就能根據不同操作新增、修改、刪除去更新列表。
protocol AddAlarmViewControllerDelegate: AnyObject {
func didAddNewAlarm()
func didUpdateAlarm()
func didDeleteAlarm()
}
我們先建立一個新的 UIViewController
,命名為 AddAlarmViewController
,並加上自己的 xib
檔案。
這個頁面會包含:
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
進入畫面時,我們要判斷是新增鬧鐘還是編輯鬧鐘:
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
}
}
}
我們讓使用者能在 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)
}
最後一步是設定本地通知。
當鬧鐘時間到時,系統就會依照使用者設定發出提示音。
// 排程通知
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)
}
}
}
今天我們讓時鐘可以新增、編輯、刪除鬧鐘,還能設定通知與重複日期。