How to apply NSCoding to a class? - swift2

I have the following class. When I try to add it to NSUserDefaults like this:
let testClass = TestClass()
testClass.Property1 = 22.3
NSUserDefaults.standardUserDefaults().setObject(testClass, forKey: "object1")
I get an error:
'NSInvalidArgumentException', reason: 'Attempt to insert non-property
list object for key object1'
class TestClass: NSObject, NSCoding {
var Property1:Double?
override init() {
super.init()
}
required init?(coder aDecoder: NSCoder) {
if let priceCoded = aDecoder.decodeObjectForKey("Property1") as? Double {
self.Property1 = priceCoded
}
}
func encodeWithCoder(aCoder: NSCoder){
if let priceEncoded = self.Property1 {
aCoder.encodeObject(priceEncoded, forKey: "Property1")
}
}
}
Any idea what I'm doing wrong?

Any idea what I'm doing wrong?
Yes. Not to state the obvious, but "attempt to insert non-property list object" pretty much covers it. The kinds of objects you can insert into the defaults system is limited to property-list types: NSString, NSDate, NSNumber, NSData, and collections NSArray and NSDictionary. You're apparently trying to insert some other kind of object, thus the error.
If you want to store some other kind of object, you'll need to somehow transform it into one of the supported types. You're on the right track -- the usual way to do that is to serialize the object using NSKeyedArchiver, which can store objects that adopt the NSCoding protocol. The idea is that you create a keyed archiver, use it to serialize the object(s) in question, and get back an instance of NSData which you can then store. To get the object back, you do the opposite: use a NSKeyedUnarchiver to deserialize the data object back into the original object.
It looks like you were expecting NSUserDefaults to serialize NSCoding-compliant objects for you, but that doesn't happen -- you need to do it yourself.
There are plenty of examples around, but the usual place to look for this would be the Archives and Serializations Programming Guide. Specifically, check out the "Creating and Extracting Archives" section for sample code. Here's a piece of the relevant example for serializing:
NSMutableData *data = [NSMutableData data];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[archiver encodeObject:aPerson forKey:ASCPersonKey];
[archiver finishEncoding];
Remember that in order to serialize an object this way, that object has to be an instance of a class that adopts the NSCoding protocol. The same document explains how to implement the necessary NSCoding methods, and there must be dozens of questions about it here on SO as well.

Related

How can I use a Transformable attribute as its actual type in a NSManagedObject subclass?

Just starting a project. The data-model file has one entity, which has a single attribute that is Transformable. It's supposed to be a NS/CGRect. I had Xcode create corresponding NSManagedObject subclass files. For "MyThing.swift", I got:
import Foundation
import CoreData
class MyThing: NSManagedObject {
// Insert code here to add functionality to your managed object subclass
}
And I got a "MyThing+CoreDataProperties.swift":
import Foundation
import CoreData
extension MyThing {
#NSManaged var myBounds: NSObject?
}
I want the property to be an actual CGRect, so I need a NSData <-> NSValue <-> NSRect conversion chain. I already have "NSKeyedUnarchiveFromDataTransformer" as the Name under the Attribute Type in Interface Builder. What do I add (and/or change) to pass CGRect values around?
Or do I not do this, and just pass NSValue-wrapped CGRects around instead? (I hope that CoreData will take care of any NSData <-> NSValue conversions.)
As long as your transformable type complies to NSCoding, Core Data will take care of the rest for you. I've used NSAttributedString in my NSManagedObjects, and only changed the CoreData provided id-type that was generated at runtime.
If you want to have a property of your own type, i.e. MyAwesomeObject, then make sure you implement initWithCoder: and encodeWithCoder:.
So, for your case, in order to store CGRects with CoreData, you would need to wrap them in a class that implements the above mentioned methods. This is because CGRect doesn't comply to the NSCoding protocol, and thus cannot be stored directly as a transformable attribute.
Your other option is of course to store x,y,width,height as properties, and either have a transient property wich you compute in awakeFromFetch and awakeFromInsert or just a convenience method.
Because CGRect is not a class but struct i would suggest creating another property which will access the raw variable with proper conversions. And then use only that property.
class MyThing {
var boundsValue : NSValue?
}
extension MyThing {
var bounds : CGRect {
get {
if let value = boundsValue {
value.CGRectValue()
}
return CGRectNull
}
set {
boundsValue = NSValue(CGRect: newValue)
}
}
}

How to store a non-standard persistent attribute in Core Data (ex. NSRect)?

I'm struggling with my first Core Data app and not having the smoothest ride :o(
I have a drawing app with a baseclass called DrawingObject which subclasses NSManagedObject and has two properties:
#NSManaged var frameAsValue: NSValue
var frame: NSRect
DrawingObject has a subclass DrawingRectangle. All have corresponding entities with fully qualified classnames set. The frameAsValue attribute is marked as transformable and frame is marked as Undefined transient. The problem is that I get an unrecognised selector error for the frameAsValue property when creating a DrawingRectangle.
I've seen suggestions to transform NSRect to a string to save it to Core Data but this seems error prone (localization) and hackish (if thats a proper word ;o). Here is the code for DrawingObject:
class DrawingObject: NSManagedObject {
#NSManaged var frameAsValue: NSValue
var frame: NSRect = NSZeroRect {
didSet {
frameAsValue = NSValue(rect: frame)
}
}
override func awakeFromInsert() {
frame = NSMakeRect(0, 0, 0, 0)
}
override func awakeFromFetch() {
frame = frameAsValue.rectValue
}
}
I'm now assuming that you have to declare all of the inherited properties in the class hierarchy in each entity. I don't have time to test this now, but will be back soon.
You have to set parent entity of DrawingRectangle to DrawingObject to be able to inherit the properties from the parent entity (just like in the class-hierarchy). You also have to set parent--child entity relationships if you intend to save different subclasses of a parent-entity/parent-class into the same to-many relationship.
For examble if you want to save different subclasses of DrawingObjects in a #NSManaged var objects: NSSet in a Drawing for example, you have to set the parent relationships on the entities to correspond to your NSManagedObject class-hierarchy. So in this case you would set the parent of the DrawingRectangle entity to DrawingObject.
Otherwise NSRect is saved as in the code shown in the question by setting the frame-property of the DrawingObject-entity to transient and optional and the frameAsValue as transformable. You should not specify a valuetransformer so Core Data will use the default value transformer which works perfectly in this case.

Unable to use reference from a Make New command in Core-data app with AppleScript support

I am able to support the Make New command of AppleScript for my app, however the returned 'specified object' (an NSUniqueIDSpecifier) for the core data managed object is useless. The following AppleScript returns the error message:
error "SpellAnalysis got an error: Invalid key form." number -10002 from level id "x-coredata:///Levels/tC5A49E01-1CE1-4ED6-8F6B-BC0AE90E279A2"
tell application "SpellAnalysis"
set thisLevel to make new «class Slev» with properties {«class Saln»:3}
get properties of thisLevel
end tell
So the newly created Levels object can not be acted upon in AppleScript. I've combed the Web for a solution to this and the closest thing I have found is Bill Cheeseman's example app, "WareroomDemo" which specifically deals with Cocoa Scriptability for Core Data apps (the Sketch example does not use Core Data). Unfortunately, it is a dated example, running only on pre-64-bit XCode and I can't actually run it--I can only look at the code. His app's Make Command may have the same limitations for all I know.
The returned 'objectSpecifier' is unable to refer to the created object either as a safe-guard against corrupting Core Data's organizing scheme, or perhaps because the returned object is an un-cashed 'fault'. I think the latter possibility is unlikely because I can force the fault to cash (by getting a property value on the managed object) , yet I get the same error message with the AppleScript.
Here is the method that creates my class:
- (id)newScriptingObjectOfClass:(Class)class forValueForKey:(NSString *)key withContentsValue:(id)contentsValue properties:(NSDictionary *)properties { // Creates a new Lesson object in response to the AppleScript 'make' command.
// Documentation for 'newScriptingObject…' states that to create a new class object when using Core Data, you intercede using the following method (or you can subclass the NSCreateCommand's 'performDefaultImplementation' method and put your NSManagedObject init code there):
if (class == [Levels class]) {
//NSLog(#"class: %#",class);
NSEntityDescription *levelsEntity = [NSEntityDescription
entityForName:#"Levels"
inManagedObjectContext:levelsDBase];
NSManagedObject *levelObject = [[NSManagedObject alloc] initWithEntity:levelsEntity insertIntoManagedObjectContext:levelsDBase];
SLOG(#"lessonObject: %#", lessonObject);
NSString *levelNumberString = [[properties objectForKey:#"levelNumber"] stringValue];
SLOG(#"levelNumberString: %#", levelNumberString);
[levelObject setValue:levelNumberString forKey:#"levelNumber"];
return levelObject; // When using Core Data, it seems that you return the newly created object directly
}
return [super newScriptingObjectOfClass:(Class)class forValueForKey:(NSString *)key withContentsValue:(id)contentsValue properties:(NSDictionary *)properties];
}
Here is my object specifier method:
- (NSScriptObjectSpecifier *)objectSpecifier {
// This NSScriptObjectSpecifiers informal protocol returns a unique ID specifier specifying the absolute string of the URI representation of this managed object. // AppleScript return value: 'level id <id>'.
// The primary container is the application.
NSScriptObjectSpecifier *containerRef = nil; // I understand that if the application is the container, this is value you use for the container reference
NSString *uniqueID = [[[self objectID] URIRepresentation] absoluteString];
return [[[NSUniqueIDSpecifier alloc] initWithContainerClassDescription:[NSScriptClassDescription classDescriptionForClass:[NSApp class]] containerSpecifier:containerRef key:#"levelsArray" uniqueID:uniqueID] autorelease];
}
The problem lies with the specifier method. The Sketch example actually uses the technique that I needed. I overlooked it many times because I didn't see how it would apply to Core Data managed objects. Instead of returning the objects uniqueID, you make it return the managedObject index using the 'indexOfObjectIdenticalTo:' method as follows:
- (NSScriptObjectSpecifier *)objectSpecifier {
NSArray *levelsArray = [[NSApp delegate] levelsArray]; // Access your exposed to-many relationship--a mutable array
unsigned index = [levelsArray indexOfObjectIdenticalTo:self]; // Determin the current objects index
if (index != (unsigned)NSNotFound) {
// The primary container is the document containing this object's managed object context.
NSScriptObjectSpecifier *containerRef = nil; // the appliation
return [[[NSIndexSpecifier allocWithZone:[self zone]] initWithContainerClassDescription:[NSScriptClassDescription classDescriptionForClass:[NSApp class]] containerSpecifier:containerRef key:#"levelsArray" index:index] autorelease];
} else {
return nil;
}
}
Note that this method resides within a subclass of your Core Data managedObject--in this case, the 'Levels' class. The 'self' within the 'indexOfObjectIndenticalToSelf:' method refers to the current managedObject ('Levels') being handled. Also, be sure to provide the specifier (accessor) type to your 'sdef' file, like this:
<element type="level">
<cocoa key="levelsArray"/>
<accessor style="index"/>
</element>

NSKeyedArchiver: distinguishing between different instances of the same class

I'm implementing support for Lion's "Resume" feature in my OS X app.
I have a custom subclass of NSViewController in which I implemented the method
encodeRestorableStateWithCoder: as:
#implementation MyClass (Restoration)
-(void)encodeRestorableStateWithCoder:(NSCoder*)coder {
[coder encodeObject:_dataMember forKey:#"object_key"]; // I get the warning below when this line is executed for the second time
}
- (void)restoreStateWithCoder:(NSCoder *)coder {
_dataMember = [coder decodeObjectForKey:#"object_key"];
}
#end
However, since I have multiple instances of MyClass, different values are saved into the same key ("object_key") and I get the following warning from Cocoa:
NSKeyedArchiver warning: replacing existing value for key
'object_key'; probable duplication of encoding keys in class hierarchy
What is the best practice to overcome this problem?
Edit: I found here that each instance automatically has its own namespace to avoid collisions, so the problem might be in the way I'm manually calling encodeRestorableStateWithCoder to different instances with the same NSCoder object without telling it that these are different instances. However, I still can't figure out how to do that properly.
Thanks in advance!
To overcome this problem, it is possible to create a new NSMutableData where each of which is written by a separate (new) NSKeyArchiver, and store them all in an array which is stored in the original NSCoder object.
Here is an example for encoding the restorable state of subitems. The decoding part can be straight-forward given this code.
- (void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
[super encodeRestorableStateWithCoder:coder];
// Encode subitems states:
NSArray* subitems = self.items;
NSMutableArray* states = [NSMutableArray arrayWithCapacity: subitems.count];
for (SubItemClass* item in subitems)
{
NSMutableData* state = [NSMutableData data];
NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:state];
[item encodeRestorableStateWithCoder:archiver];
[archiver finishEncoding];
[states addObject:state];
}
[coder encodeObject:states forKey:#"subitems"];
}

How to objects from a fetchedResultsController to a Plist?

Can someone help me. I have a coredata application but I need to save the objects from a fetchedResultsController into an NSDictionary to be used for sending UILocalNotifications.
Should I use an NSMutableSet, or a NSDictionary, or an array. I'm not used to using collections and I can't figure out the best way to do that.
Could you please give me clues on how to do that please ?
Thanks,
Mike
If I'm reading your question correctly, you're asking how you should pack objects into the userInfo dictionary of a UILocalNotification. Really, it's however works best for you; userInfo dictionaries are created by you and only consumed by you.
I'm not sure why you would be using an NSFetchedResultsController - that class is for managing the marshaling of managed objects between UI classes (like UITableView) efficiently, whereas here it sounds like you would be better off just getting an NSArray of results from your managedObjectContext and the corresponding request, like this:
NSError *error = nil;
NSArray *fetchedObjects = [myManagedObjectContext executeFetchRequest: myRequest error: &error];
if (array == nil)
{
// Deal with error...
}
where you have a pre-existing managed object context and request. You don't need to use an NSFetchedResultsController here.
From there, the simplest suggestion would be to build your userInfo dictionary like this:
NSDictionary* myUserInfo = [NSDictionary dictionaryWithObject: fetchedObjects forKey: #"AnythingYouWant"];
UILocalNotification *localNotif = [[UILocalNotification alloc] init];
// ... do other setup tasks ...
localNotif.userInfo = myUserInfo;
[[UIApplication sharedApplication] scheduleLocalNotification:localNotif];
[localNotif release];
Then when it comes time to receive that notification, you can read that dictionary like this:
- (void)application:(UIApplication *)app didReceiveLocalNotification:(UILocalNotification *)notif
{
NSArray* myFetchedObjects = [notif.userInfo objectForKey: #"AnythingYouWant"];
for(id object in myFetchedObjects)
{
// ... do other stuff ...
}
}
Now, hopefully that's clarified how the userInfo dictionary works. I don't know the details of your app, so it's hard to say, but I'm suspicious that actually passing fetched objects is NOT what you want to do here, mainly because I'm not sure that you have any guarantee that the receiving delegate method will be working with the same object context as the sending method. I would suggest perhaps putting the entity name and predicate in the dictionary and then refetching the objects at receive time with whatever the current MOC is at that moment.
Good luck!

Resources