How do I return a pre-existing Core Data object at NSCoding initialization in Swift?
Asked Answered
E

1

10

When an instance of my class is initialized using NSCoding, I want to replace it with an existing object in the Core Data database instead of calling:

super.init(entity: ..., insertIntoManagedObjectContext: ...)

as that would insert a new object into the database.

class MyClass: NSManagedObject, NSCoding {
    required init(coder aDecoder: NSCoder) {
        // Find a preexisting object in the Core Data database

        var ctx = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext!
        var fs = NSFetchRequest(entityName: "MyClass")

        // ... Configure the fetch request based on values in the decoder

        var err: NSErrorPointer = nil
        var results = ctx.executeFetchRequest(fs, error: err)

        // ... Error handling, etc

        newObject = results[0] as! MyClass

        // Attempt to replace self with the new object
        self = newObject // ERROR: "Cannot assign to 'self' in a method"
    }

    func encodeWithCoder(aCoder: NSCoder) {
        // Encode some identifying property for this object
    }
}

This was a fairly common Objective-C pattern, but I can't figure out how to replicate this in Swift 1.2, since assigning another object to self yields a compile error:

Cannot assign to 'self' in a method

and even if it did succeed, it seems Swift requires that I call NSManagedObject's designated initializer which would insert a new object into the database.


How do I replace an object with a pre-existing one in the database at initialization time? And if its not possible (please provide a reference if this is the case), what pattern should I be using instead?
Edgewise answered 12/7, 2015 at 21:4 Comment(1)
I think this idiom is simply not allowed in swift as per the "safety features" of the language.Thimblerig
M
2

You can use awakeAfterUsingCoder(_:) for replacing self:

You can use this method to eliminate redundant objects created by the coder. For example, if after decoding an object you discover that an equivalent object already exists, you can return the existing object. If a replacement is returned, your overriding method is responsible for releasing the receiver.

class Item: NSManagedObject, NSCoding {

    @NSManaged var uuid: String
    @NSManaged var name: String?

    override init(entity: NSEntityDescription, insertIntoManagedObjectContext context: NSManagedObjectContext?) {
        super.init(entity: entity, insertIntoManagedObjectContext: context)
    }

    required init(coder aDecoder: NSCoder) {
        let ctx = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext!
        let entity = NSEntityDescription.entityForName("Item", inManagedObjectContext: ctx)!

        // Note: pass `nil` to `insertIntoManagedObjectContext`
        super.init(entity: entity, insertIntoManagedObjectContext: nil)
        self.uuid = aDecoder.decodeObjectForKey("uuid") as! String
    }

    func encodeWithCoder(aCoder: NSCoder) {
        aCoder.encodeObject(self.uuid, forKey: "uuid")
    }

    override func awakeAfterUsingCoder(aDecoder: NSCoder) -> AnyObject? {
        let ctx = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext!
        let fetch = NSFetchRequest(entityName: "Item")
        fetch.predicate = NSPredicate(format: "uuid == %@", self.uuid)

        if let obj =  ctx.executeFetchRequest(fetch, error: nil)?.first as? Item {
            // OK, the object is still in the storage.
            return obj
        }
        else {
            // The object has already been deleted. so insert and use `self`.
            ctx.insertObject(self)
            return self
        }
    }
}
Melda answered 13/7, 2015 at 3:6 Comment(3)
I can't believe I missed that! Thank you!Edgewise
In case I need to establish relationships to other objects in the context (preferably at init time), would it be okay to specify a context in super.init, and then call ctx.deleteObject(self) in the case that we are substituting the entire object with one from storage?Edgewise
Warning for anyone who might come upon this post later: I've spent more time than I'm willing to admit trying to debug an issue which seems to have resulted from passing nil to the initializer for the insertIntoManagedObjectContext parameter. It seems passing nil (and inserting in awakeAfterUsingCoder instead) may have some side effects which appear to be bugs, especially in a multi-context environment.Edgewise

© 2022 - 2024 — McMap. All rights reserved.