Storing a [String] in Core Data

I'm unable to store a String array in Core Data. Things I've tried:

  • Setting the Attribute type to Transferable, Custom Class to [String], no Value transformer
  • Setting the Attribute type to Transferable, Custom Class to NSArray, no Value transformer
  • Setting the Attribute type to Transferable, Custom Class to [String], Value transformer to StringToDataTransformer below
  • Setting the Attribute type to Transferable, Custom Class to [NSAttributedString], Value transformer to AttributedStringToDataTransformer below
  • Setting the Attribute type to Transferable, Custom Class to [NSString], Value transformer to NSStringToDataTransformer below


I currently have AlarmMO.swift with the following declaration in there:

@NSManaged var notificationUuids: [String]

but I change this to whatever type is in the Custom Class during that attempt.


I'm attempting to store an array of Strings (notificationUuids) in Core Data to persist it between app launches. I've read something about custom classes that implement NSCoding to tell the program how to transfer between your data type ([String]) and the stored data type (binary data, in this new case), but I have no idea where to start with that. Is that what I need to do? Am I doing something else wrong here?


StringToDataTransformer.swift:

class AttributedStringToDataTransformer: ValueTransformer {
    
    override func transformedValue(_ value: Any?) -> Any? {
        let boxedData = try! NSKeyedArchiver.archivedData(withRootObject: value!, requiringSecureCoding: true)
        return boxedData
    }
    
    override func reverseTransformedValue(_ value: Any?) -> Any? {
        let typedBlob = value as! Data
        let data = try! NSKeyedUnarchiver.unarchivedObject(ofClasses: [String.self as! AnyObject.Type], from: typedBlob)
        return (data as! String)
    }
    
}


AttributedStringToDataTransformer.swift:

class AttributedStringToDataTransformer: ValueTransformer {
    
    override func transformedValue(_ value: Any?) -> Any? {
        let boxedData = try! NSKeyedArchiver.archivedData(withRootObject: value!, requiringSecureCoding: true)
        return boxedData
    }
    
    override func reverseTransformedValue(_ value: Any?) -> Any? {
        let typedBlob = value as! Data
        let data = try! NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSAttributedString.self], from: typedBlob)
        return (data as! String)
    }
    
}


NSStringToDataTransformer.swift:

class AttributedStringToDataTransformer: ValueTransformer {
    
    override func transformedValue(_ value: Any?) -> Any? {
        let boxedData = try! NSKeyedArchiver.archivedData(withRootObject: value!, requiringSecureCoding: true)
        return boxedData
    }
    
    override func reverseTransformedValue(_ value: Any?) -> Any? {
        let typedBlob = value as! Data
        let data = try! NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSString.self], from: typedBlob)
        return (data as! String)
    }
    
}

Accepted Reply

Hoid,


before adding specific comments to your last response, i think that we should figure out where we are right now.


-- your original question was about whether you could store and retrieve a [String] from CoreData, and you were experimenting with different types of ValueTransformers. i think that we agreed it was easy to store an [String] without the need for getting bogged down with an explicit ValueTransformer.


-- your followup question was about why you were getting a return of nil and concluding that no data was coming back from CD, but i think we got that figured out due to a simple coding issue.


but now, we're having what has become essentially a private back-and-forth conversation about lines of code -- it's not the best for forum readers, and it is now well off the original topic. additionally, you've revised CoreData entities (Set? has replaced [String]), and i also see that you've decided that NotificationUuidMO should be a subclass of AlarmMO -- that's a little suspicious to me.


on specific coding points, there's yet another class/struct in the mix, one called Alarm (one that seems to mirror but is not an AlarmMO, even though an AlarmMO gets the entity name "Alarm" in CD) that is returned from a ViewController where you transfer the details about a new alarm to CD. i don't see where .daily, .today and .tomorrow are defined, or why you would want to be using a recurrence.hashValue anywhere.


finally, i still don't have a good sense of the flow of your app, plus i've never worked with UNNotification.


so i think you still have some basic organization to do on your app, and i would offer these thoughts for you to consider to simplify some of what you have (it's a simplification that helped me).


(1) kill off the shadow "Alarm" class/struct that's moved backwards from one ViewController (which is an "AddAlarmVC") to your main VC.


(2) have the "AddAlarmVC" that's used to let the user create a new alarm add a new AlarmMO directly in CD when the user does a Save, before returning/unwinding. (there's nothing then to return -- the new alarm is already available in CD upon return.)


(3) in the ViewController that keeps your list of alarms, simply load the array of alarms from CD in viewWillAppear() and have your table reloadData(). in other words, don't make your main VC go about adding a new AlarmMO using a returned Alarm, and do not have it explicitly add a new row for the new alarm -- reloadData() will take care of all of that for you.


this will make your main ViewController code simpler and more readable.


and i'd also go back to your earlier AlarmMO layout in which you used a [String] attribute, which is where this all started.


best of luck moving on from here.


regards,

DMG

Replies

Anyone? I can never seem to get a reply on here, or stackoverflow.

Hoid,


i've done this easily without resorting to using a ValueTransformer. and since you show no code that fetches data from CD or saves data to CD, we can't tell whether it's the ValueTransformer code that's failing or your attempts to fetch and save.


here's my test this morning.


(1) i created a single view application in which i created an entity in CD named "TestEntity" having one attribute called "names" i set the attribute type to be Transformable and the custom type to be [String].


(2) the one and only view controller in the app has three textfields and a save button. when run, the one viewcontroller loads the one TestEntity from CD and saves a reference to it.


import UIKit
import CoreData

class ViewController: UIViewController {

  @IBOutlet weak var string1textfield: UITextField!
  @IBOutlet weak var string2textfield: UITextField!
  @IBOutlet weak var string3textfield: UITextField!

  fileprivate var entityForThisView: TestEntity? = nil

  override func viewDidLoad() {
    super.viewDidLoad()

    let appDelegate = UIApplication.shared.delegate as! AppDelegate
    let context = appDelegate.persistentContainer.viewContext
    let fetchRequest = NSFetchRequest<NSFetchRequestResult>()
    fetchRequest.entity = NSEntityDescription.entity(forEntityName: "TestEntity", in: context)
    guard let entities = try? context.fetch(fetchRequest) as! [TestEntity] else {
       print("Attempt to fetch data failed")     
       return     
     }

    print("Retrieved \(entities.count) entities.")
    if entities.count > 0 {
      entityForThisView = entities[0]
    } else {
      entityForThisView = NSEntityDescription.insertNewObject(forEntityName: "TestEntity", into: context) 
          as? TestEntity
      entityForThisView?.names = ["","",""]
    }
  }


(3) when the view loads, move the strings from the CD entity that was fetched to the textfields


  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    // load textfields from retrieved data
    string1textfield.text = entityForThisView?.names![0]
    string2textfield.text = entityForThisView?.names![1]
    string3textfield.text = entityForThisView?.names![2]
  }


(4) the user can go about editing the data in the textfields -- and when the user clicks the Save button, retrieve the data from the textfields and put it into the CD entry and save the context


  @IBAction func doSave(_ sender: Any) {
    // collect the three strings from the text fields into an array
    var namesArray = [String]()
    namesArray.append(string1textfield.text!)
    namesArray.append(string2textfield.text!)
    namesArray.append(string3textfield.text!)
    // assign and save to Core Data
    entityForThisView?.names = namesArray
    let appDelegate = UIApplication.shared.delegate as! AppDelegate
    appDelegate.saveContext()
  }


works fine, as far as i can see.


oh, and don't forget the obviously-needed closing } to the ViewController class definition


} // end of ViewController class definition


the usual disclaimers apply: (works for me) + (XCode 10.1) + (Swift 4.2)


good luck,

DMG

Thanks a ton for the reply! Here's how I'm loading my alarms from Core Data:

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)")
        }
        
    }


Here's how self.alarms is defined:

var alarms = [AlarmMO]()


and here's how I'm saving to Core Data:

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.notificationUuids, forKeyPath: "notificationUuids")
        
        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)
        }
        
    }


Here's an example of something that calls saveAlarm():

@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)
        }
        
    }


Another thing: I'm managing my NSManagedObject subclasses myself, so I have an AlarmMO class that subclasses NSManagedObject:

class AlarmMO: NSManagedObject {

    @NSManaged var alarmTime: Date?
    @NSManaged var alarmNumber: Int
    @NSManaged var startTimeInterval: Double
    @NSManaged var endTimeInterval: Double
    @NSManaged var recurrence: Int
    @NSManaged var notificationUuids: [String]
    
}

You don't seem to do this, but I can't be sure what the TestEntity object looks like. Can you post that?


private func deleteOldAlarms(completionHandler: @escaping () -> Void) {
        
        os_log("deleteOldAlarms() called", log: OSLog.default, type: .default)
        let notificationCenter = UNUserNotificationCenter.current()
        var activeNotificationUuids = [String]()
        var alarmsToDelete = [AlarmMO]()
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else {
            return
        }
        let managedContext = appDelegate.persistentContainer.viewContext
        
        notificationCenter.getPendingNotificationRequests(completionHandler: { (requests) in
            for request in requests {
                activeNotificationUuids.append(request.identifier)
            }
            for alarm in self.alarms {
                guard let notificationUuids = self.getNotificationUuidsFromAlarmMO(alarmMO: alarm) else {
                    os_log("Could not get notificationUuids from AlarmMO in deleteOldAlarms() in AlarmTableViewController.swift", 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)
                }
            }
            os_log("Deleting %d alarms", log: OSLog.default, type: .debug, alarmsToDelete.count)
            for alarmMOToDelete in alarmsToDelete {
                guard let notificationUuids = self.getNotificationUuidsFromAlarmMO(alarmMO: alarmMOToDelete) else {
                    os_log("Could not get notificationUuids from AlarmMO in deleteOldAlarms() in AlarmTableViewController.swift", log: OSLog.default, type: .debug)
                    return
                }
                self.removeNotifications(notificationUuids: notificationUuids)
                managedContext.delete(alarmMOToDelete)
                self.alarms.removeAll { (alarmMO) -> Bool in
                    return alarmMOToDelete == alarmMO
                }
            }
            completionHandler()
        })
        
    }


My problem is that deleteOldAlarms() above is deleting alarms that have the .daily recurrence because their notificationUuids aren't being loaded from Core Data.


It seems like the main difference between our code at this point is the entityForThisView property and its usage. In my view, an entityForThisView is analogous to an alarm, and I don't really see how what I'm doing is different from you in regards to entityForThisView other than my alarms array is public and it's an array of entities instead of just one entity. Can you shed some light here?

sorry for not posting the TestEntity class code -- it was auto-generated by XCode for me [set Codegen to Class Definition, Tools Version Minimum to Automatic (XCode 9.0)], and i thought you had enough from what i described.


there are two auto-generated files: one for the property (which has the pre-built fetchRequest, although i did not use it):


//  This file was automatically generated and should not be edited.
//

import Foundation
import CoreData


extension TestEntity {

    @nonobjc public class func fetchRequest() -> NSFetchRequest<TestEntity> {
        return NSFetchRequest<TestEntity>(entityName: "TestEntity")
    }

    @NSManaged public var names: [String]?

}


and one for the TestEntity class:


//  This file was automatically generated and should not be edited.
//

import Foundation
import CoreData

@objc(TestEntity)
public class TestEntity: NSManagedObject {

}


hopefully i'll have a chance to look through your code later ...


good luck,

DMG

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
        
    }

i'm puzzled by the last function named getNotificationUuidsFromAlarmMO in which you write


var notificationUuids: [String]? = nil
    for notificationUuidMO in notificationUuidChildren {  
            notificationUuids?.append(notificationUuidMO.notificationUuid)  
        }  
        return notificationUuids


this code will always return a nil, because each append() invocation will fail since notificationUuids is nil.


i think what you want is this, starting with an empty array and not a nil:


  var notificationUuids = [String]()  // this is an empty array
  for notificationUuidMO in notificationUuidChildren {
      notificationUuids.append(notificationUuidMO.notificationUuid)
  }
  return notificationUuids.count>0 ? notificationUuids : nil


you might even want to get rid of the ternary "this array if not empty else nil" and simply have getNotificationUuidsFromAlarmMO return a non-nil (but possibly empty) [String], depending on how it is used and whether the having no entries is really something that's a problem.


[you can be really clever if you use map() or compactMap() to assemble the return -- this function will reduce almost to one line.]


it may be that everything really was in CoreData after all, but you were not packaging the result correctly. [e.g., check notificationUuidChildren.count before you run the loop]


that should help,

DMG

DMG,


Good catch in the last method. Fixing that helped some, since now it will recognize the one notificationUuid that gets initialized when I create an alarm with recurrence of .today or .tomorrow. Now the issue is the same as when I started, where I can't get notificationUuids to save for the case of recurrence = .daily . Here's my pattern now:


@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?
    
    
}


and another class that's a subclass of AlarmMO called NotificationUuidMO. NotificationUuidMO has a relationship to AlarmMO defined as a one to one, meaning NotificationUuidMO can only have one AlarmMO parent, and an inverse relationship of one to many, meaning one AlarmMO can have many NotificationUuidMO's.


@objc(NotificationUuidMO)
public class NotificationUuidMO: AlarmMO {

    @NSManaged public var notificationUuid: String
    @NSManaged public var alarmParent: AlarmMO
    
}


Is there anything wrong with this setup?

Hoid,


before adding specific comments to your last response, i think that we should figure out where we are right now.


-- your original question was about whether you could store and retrieve a [String] from CoreData, and you were experimenting with different types of ValueTransformers. i think that we agreed it was easy to store an [String] without the need for getting bogged down with an explicit ValueTransformer.


-- your followup question was about why you were getting a return of nil and concluding that no data was coming back from CD, but i think we got that figured out due to a simple coding issue.


but now, we're having what has become essentially a private back-and-forth conversation about lines of code -- it's not the best for forum readers, and it is now well off the original topic. additionally, you've revised CoreData entities (Set? has replaced [String]), and i also see that you've decided that NotificationUuidMO should be a subclass of AlarmMO -- that's a little suspicious to me.


on specific coding points, there's yet another class/struct in the mix, one called Alarm (one that seems to mirror but is not an AlarmMO, even though an AlarmMO gets the entity name "Alarm" in CD) that is returned from a ViewController where you transfer the details about a new alarm to CD. i don't see where .daily, .today and .tomorrow are defined, or why you would want to be using a recurrence.hashValue anywhere.


finally, i still don't have a good sense of the flow of your app, plus i've never worked with UNNotification.


so i think you still have some basic organization to do on your app, and i would offer these thoughts for you to consider to simplify some of what you have (it's a simplification that helped me).


(1) kill off the shadow "Alarm" class/struct that's moved backwards from one ViewController (which is an "AddAlarmVC") to your main VC.


(2) have the "AddAlarmVC" that's used to let the user create a new alarm add a new AlarmMO directly in CD when the user does a Save, before returning/unwinding. (there's nothing then to return -- the new alarm is already available in CD upon return.)


(3) in the ViewController that keeps your list of alarms, simply load the array of alarms from CD in viewWillAppear() and have your table reloadData(). in other words, don't make your main VC go about adding a new AlarmMO using a returned Alarm, and do not have it explicitly add a new row for the new alarm -- reloadData() will take care of all of that for you.


this will make your main ViewController code simpler and more readable.


and i'd also go back to your earlier AlarmMO layout in which you used a [String] attribute, which is where this all started.


best of luck moving on from here.


regards,

DMG

DMG,


Thanks for the response. I was working on a new branch locally where I tried out using the parent-child structure of Core Data ManagedObjects. I've now gone back to master and am working with the Transformable [String] implementation you suggested.


Here's my Alarm.swift file, and showing it might shed some light on why it's its own class. It made sense to me from an OO standpoint for an Alarm to take care of all of its initialization and creation of notifications when its created, so I put all of the timing and random choice of alarm time in the Alarm.init() method. Is there a better way to go about this, or does having an Alarm class like this make sense?


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]
    let NUMBER_OF_ALLOWED_NOTIFICATIONS_CREATED_AT_ONE_TIME = 10
    
    //MARK: Initialization
    init?(alarmNumber: Int, hourStartInterval: Int, minuteStartInterval: Int, hourEndInterval: Int, minuteEndInterval: Int,
          note: String, recurrence: RecurrenceOptions) {
        
        // TODO: Include when the alarm should repeat in the Alarm properties. The options should be...
        // daily, today, or tomorrow
        
        self.alarmNumber = alarmNumber
        self.note = note
        self.recurrence = recurrence
        self.notificationUuids = [String]()
        
        let date = Date()
        let calendar = Calendar.current
        let currentDateComponents = calendar.dateComponents([.year, .month, .day, .timeZone, .hour, .minute], from: date)
        var dateComponents = DateComponents()
        dateComponents.year = currentDateComponents.year
        dateComponents.month = currentDateComponents.month
        dateComponents.timeZone = currentDateComponents.timeZone
        dateComponents.day = currentDateComponents.day
        
        let startInterval = Alarm.convertToTimeDouble(hourInterval: hourStartInterval, minuteInterval: minuteStartInterval)
        os_log("Alarm time startInterval = %f", log: OSLog.default, type: .debug, startInterval)
        let endInterval = Alarm.convertToTimeDouble(hourInterval: hourEndInterval, minuteInterval: minuteEndInterval)
        os_log("Alarm time endInterval = %f", log: OSLog.default, type: .debug, endInterval)
        self.alarmIntervalBeginTimeDouble = startInterval
        self.alarmIntervalEndTimeDouble = endInterval
        
        if endInterval < startInterval {
            os_log("Error: Alarm time endInterval is before startInterval", log: OSLog.default, type: .info)
            return nil
        }
        let alarmTimeDouble = Double.random(in: startInterval ... endInterval)
        
        let hour = Alarm.extractHourFromTimeDouble(alarmTimeDouble: alarmTimeDouble)
        let minute = Alarm.extractMinuteFromTimeDouble(alarmTimeDouble: alarmTimeDouble)
        
        os_log("Attempting to create alarm with time %d:%02d", log: OSLog.default, type: .info, hour, minute)
        dateComponents.hour = hour
        dateComponents.minute = minute

        if let alarmRandomTime = calendar.date(from: dateComponents) {
            self.alarmTime = alarmRandomTime
        }
        else {
            self.alarmTime = nil
            os_log("Failed to unwrap calendar date to alarm time in Alarm.init()", log: OSLog.default, type: .default)
        }
        
        createNotifications(dateComponents: dateComponents)
        
    }
    
    public static func convertToTimeDouble(hourInterval: Int, minuteInterval: Int) -> Double {
        
        return Double(hourInterval) + (Double(minuteInterval) / 60.0)
        
    }
    
    public static func extractHourFromTimeDouble(alarmTimeDouble: Double) -> Int {
        
        return Int(floor(alarmTimeDouble))
        
    }
    
    public static func extractMinuteFromTimeDouble(alarmTimeDouble: Double) -> Int {
        
        return Int(floor((alarmTimeDouble - floor(alarmTimeDouble)) * 60))
        
    }
    
    //MARK: Private functions
    
    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
                }
            }
        }
    }
    
    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
        
    }
    
}


Also, here's RecurrenceOptions.swift


enum RecurrenceOptions {
    
    case today, tomorrow, daily
    
    static func getValueFromIndex(index: Int) -> RecurrenceOptions {
        
        if index == 0 {
            return .today
        } else if index == 1 {
            return .tomorrow
        } else if index == 2 {
            return .daily
        } else {
            return .today
        }
        
    }
    
}

DMG,


Your comments made me go back and take a look at Alarm.swift. I notificed the reason that the notifications aren't getting created correctly. In the .daily case in createNotifications(), I have this:


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
                }
            }
}


There's a closure there that I didn't properly understand the effect of. If I'm understanding this correctly, center.getPendingNotificationRequests() is a closure and happens asynchronously, and we will get a response from this method call some time in the future, perhaps even after we have returned the alarm object and have saved it to core data. This doesn't happen in the case of .today and .tomorrow. What do you suggest I do here? 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? Sorry, but this is sort of a noob swift moment for me, I'm not yet well-versed in completion handlers and async in swift.

DMG,


I did it! I ended up taking out the call to getPendingNotificationRequests() and just adding 10 notifications at a time, thereby circumventing the async problem. Thanks for all of your help!