iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0
Mobile Development

我將點燃Swiftの大海系列 第 24

Day24. Swift一定要會のios鬧鐘復刻實作篇 (5)

  • 分享至 

  • xImage
  •  

AddAlarmView

我們今天來教大家做新增以及編輯頁面的教學
有部分與 MainView 相同的設計我就不特別講解
今天專注於新的東西!

import

import 套件的部分和 MainView 一樣需要的套件

import UIKit
import UserNotifications
import RealmSwift

IBOutlet

    //編輯頁面的刪除按鈕
    @IBOutlet weak var btnDelete: UIButton!
    //顯示新增和編輯頁面的選項:重複鬧鐘、鬧鐘名稱等的 tableView
    @IBOutlet weak var tableView: UITableView!
    //讓使用者修改鬧鐘時間的 DatePicker
    @IBOutlet weak var dpkData: UIDatePicker!

變數

    //確認目前是否在編輯介面,並讀取資料庫
    var alarmToEdit: AlarmData?
    //儲存在重複頁面中選擇要重複的日子並儲存進陣列中
    var repeatDays: [Bool] = Array(repeating: false, count: 7)
    
    var selectedSound: String = "預設"
    var alarmName: String = ""
    //設定代理,記得要設定 weak var
    weak var delegate: AlarmUpdateDelegate?
    //用來儲存是否開啟貪睡按鈕的 Bool 值
    var isSnoozeOn: Bool = true 

setUi

    func setUI() {
        // 用來設定 DatePicker 為中文的上午以及下午 identifier
        dpkData.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))
        //當為編輯模式時就取消預設的 isHidden 並顯示出來
        btnDelete.isHidden = alarmToEdit == nil
        btnDelete.setTitle("刪除鬧鐘", for: .normal)
        btnDelete.setTitleColor(.red, for: .normal)
        btnDelete.addTarget(self, action: #selector(deleteTapped), for: .touchUpInside)
        
    }

IBAction

在前面設定好 setUi 時我們同樣要設定按鈕事件
大部分寫法跟之前教過的一樣就讓各位複習一下啦!

    // 儲存按鈕事件
    @objc func doneTapped() {
        if let alarmToEdit = alarmToEdit {
            updateAlarm(alarmToEdit)
        } else {
            saveNewAlarm()
        }
    }
    
    // 取消按鈕事件
    @objc func cancelTapped() {
        self.dismiss(animated: true, completion: nil)
    }
    
    // 刪除鬧鐘按鈕事件
    @objc func deleteTapped() {
        guard let alarmToEdit = alarmToEdit else { return }
        let realm = try! Realm()
        try! realm.write {
            realm.delete(alarmToEdit)
        }
        delegate?.didDeleteAlarm()
        self.dismiss(animated: true, completion: nil)
    }

LiftCycle

記得最後要將 setUi 包進去 ViewDidLoad 裡然後要設定 tableView 的代理
以及註冊 tableViewCell 喔!
最下面我們要設一個常數去顯示如果是編輯模式的話,要直接顯示目前這個鬧鐘 cell 的資料來做更改!

        setUI()
        tableView.delegate = self
        tableView.dataSource = self
        tableView.register(UINib(nibName: "AddAlarmTableViewCell", bundle: nil), forCellReuseIdentifier: "AddAlarmTableViewCell")
        if let alarm = alarmToEdit, !alarm.isInvalidated {
            populateFields(with: alarm)
        }

Functions

首先我們要先來做排程通知的部分,我們用 NotificationCenter 去做通知彈出
sound 的部分是進階,我會在後面教學!

    //  排程鬧鐘通知
    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)
            }
        }

接下來我們要設定鬧鐘新增跟編輯後的 儲存 以及 更新 資訊

    // 儲存新的鬧鐘
    func saveNewAlarm() {
        let realm = try! Realm()
        let newAlarm = AlarmData(
            alarmTime: formatDate(dpkData.date),
            creatTime: getSystemTime(),
            name: alarmName,
            repeatDays: repeatDays,
            sound: selectedSound,
            isSnoozeOn: isSnoozeOn
        )
        try! realm.write {
            realm.add(newAlarm)
        }
        print("新增鬧鐘:", 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(dpkData.date)
            alarm.name = alarmName
            let repeatList = List<Bool>()
            repeatDays.forEach { repeatList.append($0) }
            alarm.repeatDays = repeatList
            alarm.sound = selectedSound
            alarm.isSnoozeOn = isSnoozeOn
        }
        
        UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [alarm.creatTime])
        scheduleNotification(for: alarm)
        delegate?.didEditAlarm()
        self.dismiss(animated: true, completion: nil)
    }
    

    
    // 根據 AlarmData 填充 UI 欄位
    func populateFields(with alarm: AlarmData) {
        dpkData.date = formatStringToDate(alarm.alarmTime) ?? Date()
        repeatDays = Array(alarm.repeatDays)
        selectedSound = alarm.sound
        alarmName = alarm.name
        isSnoozeOn = alarm.isSnoozeOn
    }

extensions

同樣的我們要設定 tableView 的內容
所以我們先統整我們的需求!

  • 重複天數
  • 鬧鐘名稱
  • 鬧鐘鈴聲
  • 貪睡按鈕

以上共有四個 cell 我們希望分行顯示在 tableView 上
這時候我們就需要用到 swich-case 的語法啦!
首先我們一樣的先設好 extension !

extension AddAlarmViewController: UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
    ...
}
    //我們需要四個 cell 分行顯示
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 4 }

    // 返回每個 cell
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "AddAlarmTableViewCell", for: indexPath) as! AddAlarmTableViewCell
        cell.selectionStyle = .none
        cell.lbValue?.isHidden = true
        cell.txfRename?.isHidden = true
        cell.swSnooze?.isHidden = true
        cell.txfRename?.placeholder = "鬧鐘"
        
        switch indexPath.row {
        case 0: // 重複
            cell.lbTitle.text = "重複"
            cell.lbValue?.isHidden = false
            let days = ["週日", "週一", "週二", "週三", "週四", "週五", "週六"]
            let selected = repeatDays.enumerated().compactMap { $0.element ? days[$0.offset] : nil }
            
            if repeatDays == [false, true, true, true, true, true, false] {
                cell.lbValue.text = "平日"
            } else if repeatDays == [true, false, false, false, false, false, true] {
                cell.lbValue.text = "週末"
            } else if repeatDays.allSatisfy({ $0 }) {
                cell.lbValue.text = "每天"
            } else {
                cell.lbValue.text = selected.isEmpty ? "永不" : selected.joined(separator: "、")
            }
            cell.accessoryType = .disclosureIndicator
            
        case 1: // 標籤
            cell.lbTitle.text = "標籤"
            cell.txfRename?.isHidden = false
            cell.txfRename.text = alarmName
            cell.txfRename.delegate = self
            
        case 2: // 提示聲
            cell.lbTitle.text = "提示聲"
            cell.lbValue?.isHidden = false
            cell.lbValue.text = selectedSound
            cell.accessoryType = .disclosureIndicator
            
        case 3: // 稍後提醒
            cell.lbTitle.text = "稍後提醒"
            cell.swSnooze?.isHidden = false
            cell.swSnooze.isOn = isSnoozeOn
            cell.swSnooze?.addTarget(self, action: #selector(snoozeSwitchChanged(_:)), for: .valueChanged)
            
        default: break
        }

        
        return cell
    }
    
    //重複天數以及鈴聲要在 cell 被點選時 push 出 repeatView
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        if indexPath.row == 0 {
            let repeatVC = RepeatViewController(nibName: "RepeatViewController", bundle: nil)
            repeatVC.selectedDays = repeatDays
            repeatVC.delegate = self
            self.navigationController?.pushViewController(repeatVC, animated: true)
                } else if indexPath.row == 2 {
                    let soundVC = SoundViewController(nibName: "SoundViewController", bundle: nil)
                    soundVC.currentSelectedSound = selectedSound
                    soundVC.delegate = self
                    self.navigationController?.pushViewController(soundVC, animated: true)
        }
        
    }
    
    // placeholder 設定
    func textFieldDidEndEditing(_ textField: UITextField) {
        alarmName = textField.text ?? "鬧鐘"
    }
}

最後就是重複天數和鈴聲兩個頁面的代理別忘記設定!

如果沒有要寫鈴聲的可以不用設定沒關係


extension AddAlarmViewController: RepeatViewControllerDelegate {
    func didUpdateRepeatDays(_ days: [Bool]) {
        self.repeatDays = days
        tableView.reloadRows(at: [IndexPath(row: 0, section: 0)], with: .automatic)
    }
}

extension AddAlarmViewController: SoundViewControllerDelegate {
    func didSelectSound(_ soundName: String) {
        selectedSound = soundName
        tableView.reloadRows(at: [IndexPath(row: 2, section: 0)], with: .automatic)
    }
}

protocol

protocol AlarmUpdateDelegate: AnyObject {
    func didAddNewAlarm()
    func didEditAlarm()
    func didDeleteAlarm()
}

上一篇
Day23. Swift一定要會のios鬧鐘復刻實作篇 (4)
下一篇
Day25. Swift一定要會のios鬧鐘復刻實作篇 (6)
系列文
我將點燃Swiftの大海25
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言