0

I have a lot of data stored in Core Data without any trouble. Now I need to add some additional data. Instead of creating an entity for all this new data, I have decided to use the fact that the object to be stored(and all its children) implement NSCoding, and rather store the object as a Transformable (NSObject). I have done this before (in Obj-c), but for some reason I can't get it to work this time.

Let's say I have a huge class named Event:NSObject,NSCoding which contains name, date, and a metric ton of additional variables. Then imagine you'd want the ability to let the user receive a notification a given number of days before the event starts. Like, let the user "watch" the event. I want to keep track of which events are being watched, and how long before I should send the notification. With this, I can get a list of all the "watched" events, and know how many days before the event they want a notification. This is just a poor example of the real situation, just bear with me. Don't think about the "notification" part, just storing the data.

I have my Event-object, now I have created a WatchEvent-entity in my CoreData-database which has two attributes: event:Transformable and days:Integer. Neither are optional.

My entire Event-class and all its children now implement NSCoding, so to store this in the database, I simply set it to the transformable attribute. Here you can see how I create the WatchEvent-object and put it in the database, and a function to get all WatchEvents from the DB, and a function to print out the contents of each WatchEvent.

func storeWatchEvent(someEvent:Event, numberOfDays:Int){
    let watchEvent = WatchEvent(entity: NSEntityDescription.entity(forEntityName: "WatchEvent", in: managedObjectContext)!, insertInto: managedObjectContext)
    watchEvent.days = numberOfDays //e.g 3
    watchEvent.event = someEvent
    saveContext()
}

func saveContext(){
    if managedObjectContext.hasChanges {
        do {
            try managedObjectContext.save()
        } catch {
            let nserror = error as NSError
            NSLog("Unresolved error \(nserror), \(nserror.userInfo)")
        }
    }
}

func getWatchedEvents()->[WatchEvent]?{
    return managedObjectContext.fetch(WatchEvent.fetchRequest())
}

func printOutAllWatchedEvents(){
    if let watchedEvents = getWatchedEvents(){
        watchedEvents.foreach{ (w) in
            print("numberOfDays: ", w.days)
            print("event: ", w.event)
        }
    }
}

func saveButtonClicked(){
    storeWatchEvent(someEvent, numberOfDays:3)

    // For testing, attempt to get all my events immediately, several times
    printOutAllWatchedEvents() //Prints out `numberOfDays: 3` and a valid `event` correctly
    printOutAllWatchedEvents() //Prints out `numberOfDays: 3` and a valid `event` correctly
    printOutAllWatchedEvents() //Prints out `numberOfDays: 3` and a valid `event` correctly
}

func verifyButtonClicked(){
    printOutAllWatchedEvents() //Prints out `numberOfDays: 3` and `nil` for event.
}

Let's say I have two buttons. One "Save" and another "Verify". If I click "Save", I save the valid objects, and as you can see in the code, I immediately query the database for all stored WatchEvents, and print them out. At that time, everything looks good. If I click the "verify"-button, it should've printed out the same thing, but it has deleted my event. It manages to keep the days-attribute stored, but the event is deleted. Why?:(

It doesn't matter how long I wait to click the verify-button. If I click them nearly at the same time, this still happens.

I call printOutAllWatchedEvents in saveButtonClick, three times, and since it manages to return a valid event-object every time, I assume that the storing/NSCoding-part of this was successful?

But if I click the verify-button, which I assume will happen at least a few "run loops" later, the transformable event-object has been deleted..

I have no idea what's happening.

Why does it manage to return my valid events if I request them immediately after inserting them, but not if I request them later? Why does this only affect the transformable object? The integer days is correctly retained for all WatchEvents. And since I have marked event as not optional, how can it return nil and never give me any errors? There are no errors when I call save on the context, I have checked.

Sti
  • 8,275
  • 9
  • 62
  • 124
  • 1
    Since your question has a lot to do with how it is being saved, maybe put real code in the place of `do/try managedObjectContext.save() else print out error etc. etc.` – Alec O May 23 '18 at 19:27
  • @AlecO You're right. Added now. And it never prints out any errors from saving. – Sti May 23 '18 at 19:29
  • First of all, check in your DB is that data reality stored in store coordinator (SQL) file, it's maybe happening because context you are storing and fetching are different? – Iraniya Naynesh May 23 '18 at 19:37
  • check this https://stackoverflow.com/questions/28056708/core-data-transformable-attributes-nsarray-is-empty – Iraniya Naynesh May 23 '18 at 19:52
  • I don't know what I'm looking for. I have downloaded a tool to inspect the actual SQLite-file, and I see all my WatchEvent-object, and the `event` field is *empty*. I thought this was because it got deleted somehow, since I DO get it returned in my code above if I query it immediately, but maybe it never gets stored at all, and what I receive when I query it is some sort of cache? @IraniyaNaynesh The link you provided shows the literal exact opposite of my problem. One that doesn't see the stored object at first, but gets it later. I can see them at first, but never later.. Will investigate. – Sti May 23 '18 at 20:01
  • Ok, I now tried to store a small test-object instead of my huge Event-object, and it worked.. So I guess there's something wrong with my implementation of `NSCoding`.. There are no errors to be seen anywhere, so I don't know where to look, but I should probably delete this question.. – Sti May 23 '18 at 20:13
  • So core data first save it in the persistent store and later save it to the disk, so check your coreDataStack the error will be there only, With me, it happened when I was doing in different threads but in your case, you are doing both from main thread only. I think – Iraniya Naynesh May 23 '18 at 20:14
  • @IraniyaNaynesh Figured it out.. It was a mistake in my `required init` when decoding the data :/ Posted it as an answer. – Sti May 23 '18 at 20:54

1 Answers1

0

I figured it out.. It had nothing to do with CoreData, really. It was a faulty implementation of NSCoding. I never got any errors of any kind, so it was hard to figure out, especially with so many variables, but the problem was essentially this:

class Event:NSObject,NSCoding{

    let hasBeer:Bool

    func encode(with aCoder: NSCoder) {
        aCoder.encode(hasBeer, forKey: "hasBeer")
    }
    required init?(coder aDecoder: NSCoder) {
        //This line:
        self.hasBeer = aDecoder.decodeObject(forKey: "hasBeer") as? Bool ?? false
        //Should be like this:
        self.hasBeer = aDecoder.decodeBool(forKey: "hasBeer")
        //Because it is a primitive type Bool, and not an Object of any kind.
        super.init()
    }
}

I'm still not entirely sure why I experienced it like this though..

The encoding always succeeded, so the Event was stored in the database (even though it looked empty when inspecting the blob-field in the sqlite-file using Liya). It succeeded encoding because the functions are named the same for all types (Bool, Int, Double, and NSObject). They all work with aCoder.encode(whatever, forKey: "whatever"). However, when DEcoding them, you have to pick the right decoding-function, e.g decodeBool, decodeInt32, decodeObject etc.

As I stated, I have done this in the past, in Objective-C, where I had no trouble at all, because both the encoding and decoding-functions are named for the type ([aDecoder decodeBoolForKey:@"key"]; and [aCoder encodeBool:hasBeer, forKey:@"hasBeer"];), and threw compile-time-errors when using the wrong one.

I guess I would've picked up on this if I had some fatalError("something") in my required init? instead of just using return nil when something didn't go as planned.

Sti
  • 8,275
  • 9
  • 62
  • 124