Thanks. Here's most of my AlarmTableViewController, the main view controller in my app. It shows all of the methods above, including some updated code, and how I'm using Core Data to load and save notificationUuids inside alarms and how I'm accessing notificationUuids inside NotificationUuidMO objects. The problem that I'm getting with this code is that I get the following printed out each time I create an alarm with recurrence of .today:
Creating notification for day: 4, time: 19:49, with uuid=2C9FD823-E8CF-46B5-AB4B-81259B47059E
----- notificationUuids: -----
There are no notifications for the provided AlarmMO in tableView(cellForRowAt:)
I get the notification created for the alarm, but then once tableView(cellForRowAt:) gets called, I get an empty array of notificationUuids.
I'm not sure why the pattern I'm using isn't working.
override func viewDidLoad() {
super.viewDidLoad()
requestUserNotificationsPermissionsIfNeeded()
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
loadAlarms()
for alarm in self.alarms {
os_log("There are %d notifications for alarm %d", log: OSLog.default, type: .debug, alarm.notificationUuidChildren?.count ?? 0, alarm.alarmNumber)
}
}
deinit {
NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.alarms.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: ALARM_CELL_IDENTIFIER, for: indexPath) as? AlarmTableViewCell else {
fatalError("The dequeued cell is not an instance of AlarmTableViewCell.")
}
guard let alarmMO = self.alarms[safe: indexPath.row] else {
os_log("Could not unwrap alarm for indexPath in AlarmTableViewController.swift", log: OSLog.default, type: .default)
self.tableView.reloadData()
return AlarmTableViewCell()
}
let alarmNumber = alarmMO.value(forKey: "alarmNumber") as! Int
let beginTime = alarmMO.value(forKey: "startTimeInterval") as! Double
let endTime = alarmMO.value(forKey: "endTimeInterval") as! Double
cell.alarmNumberLabel.text = "Alarm " + String(alarmNumber)
let beginTimeHour = Alarm.extractHourFromTimeDouble(alarmTimeDouble: beginTime)
let beginTimeMinute = Alarm.extractMinuteFromTimeDouble(alarmTimeDouble: beginTime)
cell.beginTimeLabel.text = formatTime(hour: beginTimeHour, minute: beginTimeMinute)
let endTimeHour = Alarm.extractHourFromTimeDouble(alarmTimeDouble: endTime)
let endTimeMinute = Alarm.extractMinuteFromTimeDouble(alarmTimeDouble: endTime)
cell.endTimeLabel.text = formatTime(hour: endTimeHour, minute: endTimeMinute)
os_log("----- notificationUuids: -----", log: OSLog.default, type: .debug)
if let notificationUuids = self.getNotificationUuidsFromAlarmMO(alarmMO: alarmMO) {
for uuid in notificationUuids {
os_log("uuid: %@", log: OSLog.default, type: .debug, uuid)
}
} else {
os_log("There are no notifications for the provided AlarmMO in tableView(cellForRowAt:)", log: OSLog.default, type: .debug)
return cell
}
return cell
}
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) {
if (editingStyle == .delete) {
guard let alarm = self.alarms[safe: indexPath.row] else {
os_log("Could not get alarm from its indexPath in AlarmTableViewController.swift", log: OSLog.default, type: .default)
self.tableView.reloadData()
return
}
if let notificationUuids = self.getNotificationUuidsFromAlarmMO(alarmMO: alarm) {
self.removeNotifications(notificationUuids: notificationUuids)
} else {
os_log("There are no notifications for the provided AlarmMO in tableView(forRowAt:)", log: OSLog.default, type: .debug)
}
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
managedContext.delete(alarm)
self.alarms.remove(at: indexPath.row)
for (index, alarm) in self.alarms.enumerated() {
let alarmNumber = index + 1
alarm.setValue(alarmNumber, forKey: "alarmNumber")
}
self.saveContext()
self.tableView.reloadData()
}
}
// MARK: Actions
@IBAction func unwindToAlarmList(sender: UIStoryboardSegue) {
if let sourceViewController = sender.source as? AddAlarmViewController, let alarm = sourceViewController.alarm {
let newIndexPath = IndexPath(row: self.alarms.count, section: 0)
saveAlarm(alarmToSave: alarm)
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
}
// MARK: Private functions
@objc private func didBecomeActive() {
deleteOldAlarms {
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
private func deleteOldAlarms(completionHandler: @escaping () -> Void) {
os_log("deleteOldAlarms() called", log: OSLog.default, type: .default)
let notificationCenter = UNUserNotificationCenter.current()
var alarmsToDelete = [AlarmMO]()
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
notificationCenter.getPendingNotificationRequests(completionHandler: { (requests) in
alarmsToDelete = self.calculateAlarmsToDelete(requests: requests)
os_log("Deleting %d alarms", log: OSLog.default, type: .debug, alarmsToDelete.count)
for alarmMOToDelete in alarmsToDelete {
if let notificationUuids = self.getNotificationUuidsFromAlarmMO(alarmMO: alarmMOToDelete) {
self.removeNotifications(notificationUuids: notificationUuids)
} else {
os_log("There are no notifications for the provided AlarmMO in deleteOldAlarms()", log: OSLog.default, type: .debug)
return
}
managedContext.delete(alarmMOToDelete)
self.alarms.removeAll { (alarmMO) -> Bool in
return alarmMOToDelete == alarmMO
}
}
completionHandler()
})
}
private func calculateAlarmsToDelete(requests: [UNNotificationRequest]) -> [AlarmMO] {
var activeNotificationUuids = [String]()
var alarmsToDelete = [AlarmMO]()
for request in requests {
activeNotificationUuids.append(request.identifier)
}
for alarm in self.alarms {
guard let notificationUuids = self.getNotificationUuidsFromAlarmMO(alarmMO: alarm) else {
os_log("There are no notifications for the provided AlarmMO in calculateAlarmsToDelete()", log: OSLog.default, type: .debug)
return []
}
let activeNotificationUuidsSet: Set = Set(activeNotificationUuids)
let alarmUuidsSet: Set = Set(notificationUuids)
let union = activeNotificationUuidsSet.intersection(alarmUuidsSet)
if union.isEmpty {
alarmsToDelete.append(alarm)
}
}
return alarmsToDelete
}
private func removeNotifications(notificationUuids: [String]) {
os_log("Removing %d alarm notifications", log: OSLog.default, type: .debug, notificationUuids.count)
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.removePendingNotificationRequests(withIdentifiers: notificationUuids)
}
private func loadAlarms() {
os_log("loadAlarms() called", log: OSLog.default, type: .debug)
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let fetchRequest = NSFetchRequest(entityName: "Alarm")
do {
if self.alarms.count == 0 {
self.alarms = try managedContext.fetch(fetchRequest)
os_log("Loading %d alarms", log: OSLog.default, type: .debug, self.alarms.count)
} else {
os_log("Didn't need to load alarms", log: OSLog.default, type: .debug)
}
} catch let error as NSError {
print("Could not fetch alarms. \(error), \(error.userInfo)")
}
}
private func saveAlarm(alarmToSave: Alarm) {
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
return
}
let managedContext = appDelegate.persistentContainer.viewContext
let entity = NSEntityDescription.entity(forEntityName: "Alarm", in: managedContext)!
let alarmMO = AlarmMO(entity: entity, insertInto: managedContext)
alarmMO.setValue(alarmToSave.alarmTime, forKeyPath: "alarmTime")
alarmMO.setValue(alarmToSave.alarmNumber, forKeyPath: "alarmNumber")
alarmMO.setValue(alarmToSave.alarmIntervalBeginTimeDouble, forKeyPath: "startTimeInterval")
alarmMO.setValue(alarmToSave.alarmIntervalEndTimeDouble, forKeyPath: "endTimeInterval")
alarmMO.setValue(alarmToSave.recurrence.hashValue, forKeyPath: "recurrence")
for notificationUuid in alarmToSave.notificationUuids {
let entity = NSEntityDescription.entity(forEntityName: "NotificationUuid", in: managedContext)!
let notificationUuidMO = NotificationUuidMO(entity: entity, insertInto: managedContext)
notificationUuidMO.notificationUuid = notificationUuid
notificationUuidMO.alarmParent = alarmMO
alarmMO.addToNotificationUuidChildren(notificationUuidMO)
}
if managedContext.hasChanges {
do {
try managedContext.save()
self.alarms.append(alarmMO)
} catch let error as NSError {
print("Could not save alarm to CoreData. \(error), \(error.userInfo)")
}
} else {
os_log("No changes to the context to save", log: OSLog.default, type: .debug)
}
}
private func getNotificationUuidsFromAlarmMO(alarmMO: AlarmMO) -> [String]? {
guard let notificationUuidChildren = alarmMO.notificationUuidChildren else {
os_log("Returned no notificationUuidChildren in getNotificationUuidsFromAlarmMO() in AlarmTableViewController.swift", log: OSLog.default, type: .debug)
return nil
}
var notificationUuids: [String]? = nil
for notificationUuidMO in notificationUuidChildren {
notificationUuids?.append(notificationUuidMO.notificationUuid)
}
return notificationUuids
}