I'm trying to save strings as properties of certain entities and those entities are using relationships. Here's the model...
I have an entity and it's managed by a subclass of NSManagedObject called AlarmMO. This is what that looks like:
@objc(AlarmMO)
public class AlarmMO: NSManagedObject {
@NSManaged public var alarmNumber: Int64
@NSManaged public var alarmTime: NSDate?
@NSManaged public var endTimeInterval: Double
@NSManaged public var recurrence: Int64
@NSManaged public var note: String?
@NSManaged public var startTimeInterval: Double
@NSManaged public var notificationUuidChildren: Set<NotificationUuidMO>?
}
As you can see, AlarmMO objects have notificationUuidChildren, which is a Set of NotificationUuidMO objects. NotificationUuid looks like this:
@objc(NotificationUuidMO)
public class NotificationUuidMO: AlarmMO {
@NSManaged public var notificationUuid: String
@NSManaged public var alarmParent: AlarmMO
}
NotificationUuidMO is a subclass of AlarmMO, so each NotificationUuidMO has one AlarmMO parent, and AlarmMO objects have the option of having NotificationUuidMO objects as "children". You gettin this, camera guy?
Here is almost all of the main AlarmTableViewController class that uses the Core Data stuff:
//MARK: Properties
var alarms = [AlarmMO]()
let ALARM_CELL_IDENTIFIER = "AlarmTableViewCell"
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)
resetAlarmNumbers()
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)
}
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)
resetAlarmNumbers()
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 resetAlarmNumbers() {
for (index, alarm) in self.alarms.enumerated() {
let alarmNumber = index + 1
alarm.setValue(alarmNumber, forKey: "alarmNumber")
}
}
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<String> = Set(activeNotificationUuids)
let alarmUuidsSet: Set<String> = Set(notificationUuids)
let union = activeNotificationUuidsSet.intersection(alarmUuidsSet)
if union.isEmpty {
alarmsToDelete.append(alarm)
}
}
return alarmsToDelete
}
private func formatTime(hour: Int, minute: Int) -> String {
let amPm = formatAmPm(hour: hour)
let hourStr = formatHour(hour: hour)
let minuteStr = formatMinute(minute: minute)
return hourStr + ":" + minuteStr + amPm
}
private func formatAmPm(hour: Int) -> String {
if hour < 13 {
return "am"
} else {
return "pm"
}
}
private func formatHour(hour: Int) -> String {
if hour == 0 {
return "12"
} else if hour > 12 {
return String(hour - 12)
} else {
return String(hour)
}
}
private func formatMinute(minute: Int) -> String {
if minute < 10 {
return "0" + String(minute)
} else {
return String(minute)
}
}
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<AlarmMO>(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")
alarmMO.setValue(alarmToSave.note, forKeyPath: "note")
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]()
for notificationUuidMO in notificationUuidChildren {
notificationUuids.append(notificationUuidMO.notificationUuid)
}
os_log("Returning %d notificationUuids in getNotificationUuidsFromAlarmMO() in AlarmTableViewController.swift", log: OSLog.default, type: .debug, notificationUuids.count)
return notificationUuids
}
The main parts to look at are viewDidLoad(), the tableView datasource methods, deleteOldAlarms(), loadAlarms(), saveAlarm(), and getNotificationsFromAlarmMO(). This is what Alarm generally defines, from a public level:
class Alarm {
//MARK: Properties
var alarmTime: Date?
var alarmNumber: Int
var alarmIntervalBeginTimeDouble: Double
var alarmIntervalEndTimeDouble: Double
var note: String
var recurrence: RecurrenceOptions
var notificationUuids: [String]
...
}
As you can see, each alarm has notificationUuids that correspond to the notifications that alarm is responsible for.
HERE'S MY PROBLEM: I can save ONE NotificationUuidMO object to the Set of NotificationUuidMO objects that is the notificationUuidChildren of an AlarmMO object, but not more than one. For some reason, when I create an alarm with the recurrence value of .daily, which creates more than one notification, NONE of the notifications get saved as NotificationUuidMO objects under the alarm. And since deleteOldAlarms() deletes alarms with no notifications scheduled for them, these alarms get deleted immediately after creation. Not cool.
What could be causing this behavior?
IMPORTANT EDIT: I've now realized from comments below that Alarm.swift has a call to notificationCenter.getPendingNotificationRequests(), which is an async method and takes a completion handler. I'm adding all of my notifications AFTER I return back to the other view controller and AFTER the alarm is saved to core data.
private func createNotifications(dateComponents: DateComponents) {
switch (recurrence) {
case .today:
createNotification(for: dateComponents)
case .tomorrow:
createNotification(for: day(after: dateComponents))
case .daily:
let center = UNUserNotificationCenter.current()
center.getPendingNotificationRequests { (notifications) in
var numberOfCreatableNotifications = 64 - notifications.count
var numberOfCreatedNotifications = 0
var currentDay: DateComponents? = dateComponents
while numberOfCreatableNotifications > 0
&& numberOfCreatedNotifications < self.NUMBER_OF_ALLOWED_NOTIFICATIONS_CREATED_AT_ONE_TIME {
self.createNotification(for: currentDay)
currentDay = self.day(after: currentDay)
numberOfCreatableNotifications -= 1
numberOfCreatedNotifications += 1
}
}
}
}
How should I handle this? Should I get rid of the call to getPendingNotificationRequests() and just try to create notifications if I can, and not care how many possible notifications I have left to create? Or should I do my saving to core data inside this completion handler somehow?