Local notifications used to fire for me in the simulator until I basically rewrote my app using a different data model. Now, even though everything seems to be happening that needs to happen for a notification to fire, it's not.
Here's what I can confirm show up in the logs, in order:
"User has notifications permissions enabled"
"User enabled notifications permissions"
"Attempting to create alarm with time 19:37"
"Creating notification for day: 19, time: 19:36, with uuid=B76AA489-CF41-49CB-9C3D-CF48590A9933"
"There are 1 notificationUuids attached to the alarm created"
"----- notificationUuids: -----"
"uuid: B76AA489-CF41-49CB-9C3D-CF48590A9933"
No errors are printed. This leads me to believe that the alarm is getting created properly, and the notification is getting added to the notification center, but it's not firing for some reason. Below is my code, abridged to reduce unneeded complexity. I only removed things that aren't related to notifications.
class AlarmTableViewController: UITableViewController {
//MARK: Public properties
var alarms = [Alarm]()
let ALARM_CELL_IDENTIFIER = "AlarmTableViewCell"
override func viewDidLoad() {
super.viewDidLoad()
requestUserNotificationsPermissionsIfNeeded()
NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(didReceiveNotification), name: NotificationNames.randomAlarmNotification, object: nil)
loadAlarms()
for alarm in self.alarms {
os_log("There are %d notifications for alarm %d", log: OSLog.default, type: .debug, alarm.notificationUuids.count, alarm.alarmNumber)
}
}
deinit {
NotificationCenter.default.removeObserver(self, name: UIApplication.didBecomeActiveNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: NotificationNames.randomAlarmNotification, object: nil)
}
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 alarm = 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 = alarm.value(forKey: "alarmNumber") as! Int
cell.alarmNumberLabel.text = "Alarm " + String(alarmNumber)
let beginTime = alarm.value(forKey: "startTimeInterval") as! Double
let endTime = alarm.value(forKey: "endTimeInterval") as! Double
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)
guard let notificationUuids = self.getNotificationUuidsFromAlarmMO(alarm: alarm) else {
os_log("Could not get notificationUuids from AlarmMO in tableView(cellForRowAt:) in AlarmTableViewController.swift", log: OSLog.default, type: .debug)
return cell
}
os_log("----- notificationUuids: -----", log: OSLog.default, type: .debug)
for uuid in notificationUuids {
os_log("uuid: %@", log: OSLog.default, type: .debug, uuid)
}
return cell
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let alarmNotificationIdentifier = response.notification.request.identifier
var alarmActedOn: Alarm? = nil
for alarm in self.alarms {
guard let notificationUuids = self.getNotificationUuidsFromAlarmMO(alarm: alarm) else {
os_log("Could not get notificationUuids from AlarmMO in tableView(forRowAt:) in AlarmTableViewController.swift", log: OSLog.default, type: .debug)
return
}
for notificationUuid in notificationUuids {
if notificationUuid == alarmNotificationIdentifier {
alarmActedOn = alarm
}
}
}
if response.actionIdentifier == "snooze" {
alarmActedOn?.setNewRandomAlarmTime()
}
completionHandler()
}
// 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)
os_log("There are %d notificationUuids attached to the alarm created", log: OSLog.default, type: .debug, alarm.notificationUuids.count)
saveAlarm(alarmToSave: alarm)
tableView.insertRows(at: [newIndexPath], with: .automatic)
}
}
// MARK: Private functions
@objc private func didReceiveNotification() {
os_log("entered the function", log: OSLog.default, type: .debug)
}
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<Alarm>(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 alarm = Alarm(entity: entity, insertInto: managedContext)
alarm.setValue(alarmToSave.alarmTime, forKeyPath: "alarmTime")
alarm.setValue(alarmToSave.alarmNumber, forKeyPath: "alarmNumber")
alarm.setValue(alarmToSave.startTimeInterval, forKeyPath: "startTimeInterval")
alarm.setValue(alarmToSave.endTimeInterval, forKeyPath: "endTimeInterval")
alarm.setValue(RecurrenceOptions.getRawValue(value: alarmToSave.recurrence), forKeyPath: "recurrenceIndex")
alarm.setValue(alarmToSave.notificationUuids, forKeyPath: "notificationUuids")
alarm.setValue(alarmToSave.note, forKeyPath: "note")
if managedContext.hasChanges {
do {
try managedContext.save()
self.alarms.append(alarm)
} 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(alarm: Alarm) -> [String]? {
guard let notificationUuids = alarm.value(forKey: "notificationUuids") as! [String]? else {
os_log("Found nil when attempting to unwrap notificationUuids in getNotificationUuidsFromAlarmMO() in AlarmTableViewController.swift, returning nil",
log: OSLog.default, type: .default)
return nil
}
return notificationUuids
}
private func requestUserNotificationsPermissionsIfNeeded() {
let notificationCenter = UNUserNotificationCenter.current()
notificationCenter.getNotificationSettings { (settings) in
guard settings.authorizationStatus == .authorized else {
return
}
os_log("User has notifications permissions enabled", log: OSLog.default, type: .debug)
}
notificationCenter.requestAuthorization(options: [.alert, .sound, .badge]) {
(granted, error) in
if !granted {
self.presentNotificationsPermissionDenialConfirmationAlert()
} else {
os_log("User enabled notifications permissions", log: OSLog.default, type: .debug)
}
}
return
}
private func presentNotificationsPermissionDenialConfirmationAlert() {
let alert = UIAlertController(title: "Are you sure you don't want to allow notifications?",
message: "The application cannot function without notifications.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Yes", style: .cancel, handler: {
action in
fatalError("User declined to allow notifications permissions")
}))
alert.addAction(UIAlertAction(title: "No", style: .default, handler: {
action in
self.requestUserNotificationsPermissionsIfNeeded()
}))
self.present(alert, animated: true)
}
}
That is my AlarmTableViewController code. Here's my abridged Alarm.swift code:
class Alarm: NSManagedObject {
@NSManaged var alarmTime: Double
@NSManaged var alarmNumber: Int
@NSManaged var startTimeInterval: Double
@NSManaged var endTimeInterval: Double
@NSManaged var note: String
@NSManaged var notificationUuids: [String]
@NSManaged var recurrenceIndex: Int
let NUMBER_OF_ALLOWED_NOTIFICATIONS_CREATED_AT_ONE_TIME = 10
}
extension Alarm {
static func newAlarm(context: NSManagedObjectContext, alarmNumber: Int, timeIntervals: TimeIntervals, note: String, recurrence: RecurrenceOptions) -> Alarm? {
let startInterval = Alarm.convertToTimeDouble(hour: timeIntervals.hourStartInterval, minute: timeIntervals.minuteStartInterval)
let endInterval = Alarm.convertToTimeDouble(hour: timeIntervals.hourEndInterval, minute: timeIntervals.minuteEndInterval)
if endInterval < startInterval {
os_log("Error: Alarm time endInterval is before startInterval", log: OSLog.default, type: .info)
return nil
}
let newAlarm = Alarm(context: context)
newAlarm.alarmNumber = alarmNumber
newAlarm.note = note
newAlarm.recurrenceIndex = RecurrenceOptions.getRawValue(value: recurrence)
newAlarm.notificationUuids = [String]()
newAlarm.startTimeInterval = startInterval
newAlarm.endTimeInterval = endInterval
newAlarm.setNewRandomAlarmTime()
return newAlarm
}
var recurrence: RecurrenceOptions {
get {
return RecurrenceOptions.getValueFromIndex(index: recurrenceIndex)
}
set {
self.recurrenceIndex = RecurrenceOptions.getRawValue(value: newValue)
}
}
var hour: Int {
return Int(floor(self.alarmTime))
}
var minute: Int {
return Int(round((self.alarmTime - floor(self.alarmTime)) * 60))
}
func setNewRandomAlarmTime() {
let startInterval = self.startTimeInterval
let endInterval = self.endTimeInterval
let currentDateAndTime = Date()
let currentDateAndTimeComponents = Calendar.current.dateComponents([.hour, .minute], from: currentDateAndTime)
guard let currentHour = currentDateAndTimeComponents.hour else {
os_log("Could not unwrap currentDateAndTimeComponents.hour in Alarm.setNewRandomAlarmTime()", log: OSLog.default, type: .default)
return
}
guard let currentMinute = currentDateAndTimeComponents.minute else {
os_log("Could not unwrap currentDateAndTimeComponents.minute in Alarm.setNewRandomAlarmTime()", log: OSLog.default, type: .default)
return
}
let currentTimeDouble = Alarm.convertToTimeDouble(hour: currentHour, minute: currentMinute)
// We should start the random alarm interval from the current
// time if the current time is past the startInterval already
if currentTimeDouble > startInterval {
self.alarmTime = Double.random(in: currentTimeDouble ... endInterval)
} else {
self.alarmTime = Double.random(in: startInterval ... endInterval)
}
scheduleNotifications()
}
func scheduleNotifications() {
os_log("Attempting to create alarm with time %d:%02d", log: OSLog.default, type: .info, self.hour, self.minute)
let date = Date()
let calendar = Calendar.current
let currentDateComponents = calendar.dateComponents([.year, .month, .day, .timeZone, .hour, .minute], from: date)
createNotifications(dateComponents: currentDateComponents)
}
//MARK: Private functions
private func createNotifications(dateComponents: DateComponents) {
switch (self.recurrence) {
case .today:
self.createNotification(for: dateComponents)
case .tomorrow:
self.createNotification(for: self.day(after: dateComponents))
case .daily:
var numberOfCreatedNotifications = 0
var currentDay: DateComponents? = dateComponents
while numberOfCreatedNotifications < self.NUMBER_OF_ALLOWED_NOTIFICATIONS_CREATED_AT_ONE_TIME {
self.createNotification(for: currentDay)
currentDay = self.day(after: currentDay)
numberOfCreatedNotifications += 1
}
}
}
private func createNotification(for dateComponents: DateComponents?) {
let center = UNUserNotificationCenter.current()
let content = UNMutableNotificationContent()
content.title = "Random Alarm"
content.subtitle = "It's time!"
content.body = self.note
content.sound = UNNotificationSound.default
guard let dateComponents = dateComponents else {
os_log("Could not unwrap dateComponents in createNotification() in Alarm.swift", log: OSLog.default, type: .debug)
return
}
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false)
let uuidString = UUID().uuidString
let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger)
self.notificationUuids.append(uuidString)
guard let day = dateComponents.day else {
os_log("Could not unwrap dateComponents.day in createNotification() in Alarm.swift", log: OSLog.default, type: .debug)
return
}
guard let hour = dateComponents.hour else {
os_log("Could not unwrap dateComponents.hour in createNotification() in Alarm.swift", log: OSLog.default, type: .debug)
return
}
guard let minute = dateComponents.minute else {
os_log("Could not unwrap dateComponents.minute in createNotification() in Alarm.swift", log: OSLog.default, type: .debug)
return
}
os_log("Creating notification for day: %d, time: %d:%02d, with uuid=%s", log: OSLog.default, type: .debug, day, hour, minute, uuidString)
center.add(request) { (error) in
if let err = error {
print("error \(err.localizedDescription)")
}
}
}
private func day(after dateComponents: DateComponents?) -> DateComponents? {
let calendar = Calendar.autoupdatingCurrent
guard let dateComponents = dateComponents,
let date = calendar.date(from: dateComponents),
let tomorrow = calendar.date(byAdding: .day, value: 1, to: date)
else {
os_log("Could not calculate tomorrow in Alarm.swift", log: OSLog.default, type: .debug)
return nil
}
let newDateComponents = calendar.dateComponents([.year, .month, .day, .timeZone, .hour, .minute], from: tomorrow)
return newDateComponents
}
}