NSKeyedArchiver and AttributedString [duplicate] - objective-c

I am trying to store an NSAttributedString to a Core Data SQL store.
I have the property set as a "transformable", it is optional and it is NOT transient or indexed and the value transformer name is set to default "NSKeyedUnarchiveFromData". In the .xcdatamodel and generated the managed object class which has this in the .h:
#property (nonatomic, retain) id Text; (I have tried changing id to NSAttributedString *Text)
and this in the .m:
#dynamic Text;
I look through and set the ".text" property of my NSManagedObject to the attributed string then when completed I do:
NSError *error = nil;
[managedObjectContext save:&error];
This through is causing this error in the output:
[NSCFType encodeWithCoder:]:
unrecognized selector sent to instance
0xc04edb0 Terminating app due to
uncaught exception
'NSInvalidArgumentException', reason:
'* -[NSCFType encodeWithCoder:]:
unrecognized selector sent to instance
0xc04edb0'
I have checked the class of what I am storing to the property and it is NSAttributedString also I check responsesToSelector #selector(:) and this returns true so very confused as this is contrary to the error message?
Please advise.
Thanks
James

For anyone experiencing this problem I found the easiest solution:
In Core Data add an attribute, let's call it attributedText. Then define its type as Transformable. After you create the .h file, change the data type of attributedText from NSDictionary to NSAttributedString.
Now you can save the NSAttributedString in Core Data with no modification needed.
Recalling it is as easy as going:
myObject.attributedText
which will return your NSAttributedString!
Hope this helps someone.

I was checking the Apple Developer Forums and found a thread almost exactly the same as this question, one person had done this but unfortunately did not share the code. All they said was the following:
"In Core Data i have an transformable in the database and i us my own NSVauleTransformer. This is a subclass of NSValueTransformer and creates an attributed string from the data object and back.
Therefore i created a class called PersistableAttributedString which is NSCoding compliant. This class has a string and an array of attributes and builds the attributed string. I also created a class for the possible text attributes which is NSCoding compliant. Together the string and the attributes are all NSCoding compliant.
The class NSAttributedString is also NSCoding compliant, but the attributes are not, that's the problem."
Hope that might help.

In Xcode 10 with automatic code generation this is a lot simpler than the other suggestions.
Select the name of the Attribute and open the Data Model inspector (Command+Option+3)
Set Attribute Type to Transformable
Set Custom Class to NSAttributedString
And that's it, now you can just save your data in your Swift code as you'd expect, e.g.:
detailItem.content = textView.attributedText

Another idea would be to create a Custom Class of NSAttributedString and somewhere use enumerateAttributesInRange:options:usingBlock: to get all the attributes of the string and then save the NSDictionary with the attributes and ranges in to Core Data aswell as the attributed string stripped of it's attributes.
Then when the string is loaded again you could apply the attributes that are in the dictionary to the attributed string using initWithString:attributes:.

It's the font that's giving you grief - a CTDictionary is toll-free bridged to NSDictionary which implements NSCoding so should encode fine.
You might have to deal with the font yourself :( - here's a sucky way of doing it.
1) Instead of storing the NSAttributedString, break it down and put each of it's components into an array.
2) Go through the array - if you see font ref you must store just the information required to re-create this font - have a look at the CTFontCopyFontDescriptor function and the CTFontDescriptorCopyAttribute function should let you get font attributes as a string. Put all these into a NSDictionary which should store in core data fine.
3) Store this array in core data - hopefully all the items in the array will be NSCoding compliant so you should be fine.
...
To recreate your string, when you load from coredata, if you see an NSDctionary representing font attributes you should be able to re-create the fCTFontDescriptor and from that the font.
Then, put your string back together.

I found a way to save attributed text in Swift 4 that does not use a header file. My Core Data store consists of an entity called "AttrNote" with attributes of "title" and "notes". "title" is of type "String" but "notes" is of type "transformable". Then within the view controller where note entry/editing is done I have the following for the save button:
#IBAction func saveBtn(_ sender: Any) {
var note: AttrNote!
//other code
if let title = titleTextField.text {
note.title = title
}
if let noteText = notesTextView.attributedText {
note.notes = noteText
}
And the function that is called to load the data has the following:
func loadNoteData() {
if let note = noteToEdit {
titleTextField.text = note.title
notesTextView.attributedText = note.notes as! NSAttributedString
}
}
And I have the following in ViewDidLoad that enables the B/I/U formatting options to appear with the selection of text:
notesTextView.allowsEditingTextAttributes=true
I am able to save attributed text and then view/edit it later.

OK... Some kind of break through although not a good one...
If we NSLog the attributed string then we can see in there it has NSFont and NSParagraphStyle in. Although these are NOT NSFont and NSParagraphStyle in the code these are CTFontRef and CT Dictionaries of paragraph styles... These are not NS objects although in NSLog they output as these and therefore guess that is why we can not perform the "encodeWithCoder" selector on the object.
IF in the code we just do "NSFont;" the compiler says "NSFont undeclared" so what can we do as we only have the CT functions?
As my colleague said in the comments above if we set the ".text" property to just "NSAttrinutedString *string = [NSAttributedString alloc] initWithString:#"test"] it saves fine and if we remove all the styling from the one we WANT to save it also works!
THERE MUST BE A WAY OF STORING NSATTRIBUTED STRING WITH STYLING INTO CORE DATA... NO?
Any ideas greatly appreciated.
-JM

Related

NSAttrbutedText object output's a raw string instead of rich text in my NSTextField

I have an NSTextField in my XIB, and I created a NSAttributedString instance, and used initWithHTML:documentAttributes: with an NSData object of some html with bold and italic etc.
I used setAttributedText and it outputs plain text of the object:
I am using PyObj-C bridge for my code, so here's my code:
html = """<p><b>Word</b> - English</p>
<p><i>Phonetics</i> - Noun</p>
<p><i>A love this word!</i></p>
<p>Definition - An abstraction</p>
"""
new_html = str.encode(html) # Turn html into byte code for NSData
# Make an NSData object with the html string in it
html = Cocoa.NSData.alloc().initWithBytes_length_(new_html, len(new_html))
# Make an instance of an Attributed String
attrString = Foundation.NSAttributedString.alloc().init()
# Instantiate attributed string with the NSData html string
definition = attrString.initWithHTML_documentAttributes_(html, None)
self.definitionField.setAttributedStringValue_(definition)
Am I doing something wrong? I've looked everywhere on the web, cant seem to find a forum post with my problem too.
The second argument of initWithHTML_documentAttributes_ is a pass-by-reference in-out argument in Objective-C. Because of this the Python version of this method returns a tuple of two values: the actual return value and the output version of the second argument.
To get the behaviour you want use:
definition, _ = Foundation.NSAttributedString.initWithHTML_documentAttributes_(html, objc.NULL)
This also sets the second argument to objc.NULL instead of None to tell the method that your not interested in getting a dict with attributes.
Some other notes:
You're first initialising the attributed with the init method, and then reinitialise it. That's not documented as a valid way to initialise the object, it is better to call just one init* method.
It is not necessary to explicitly create an NSData object, PyObjC proxies instances of bytes as NSData objects. This simplifies your code.

GRMustache core data category not visible?

I've a bunch of core data models 'NSManagedObject subclasses' with a couple of categories for each.
I want the template to use attributes from model's category, which is from what I understand is ok since:
[model valueForKey:#"attributeName"];
is actually returning the desired value!
BUT when I evaluate it in a mustache template it returns an empty string!!
so this code snippet is returning an empty string:
NSString *template = #"{{# experiences }}{{experienceElement}}{{/ experiences }}";
return [GRMustacheTemplate renderObject:self fromString:template error:nil];
where self is another model's category that has experiences as an attribute (NSSet) and experienceElement is a function that returns a string in Experience Model's category.
but it works when I replace experienceElement with a real attribute in the Experience model it self!
what is wrong on this implementation?
note: this is a cocoa mac app, using libGRMustache7-MacOS.a
Starting from v 7.0.0 for the sake of security
A new safe key access constraint was introduced to limit keys to only class properties
A solution is to declare a read only property at the category with the name of the method
at category_file.h
#property (nonatomic, readonly) NSString * experienceElement
at category_file.m
-(NSString*) experienceElement
{
NSString *newValue = #"";//derive new value
return newValue;
}

NSString with fixed length

I want an nsstring object that can only store specified lenght of character in it.
If it exceeds it should get truncated from left. For example if i set lenght to 5, and I enter value as Ileana then it should store leana.
I tried by making a category on nsstring but i am out of ideas :
-(void)setMaximumLength:(NSInteger)length;{
if ([self length]>length) {
NSLog(#"exxed");
}
}
Please suggest what should I do? I hve one thng in my mind I need to observe the string size but how to do in a category, and which notification will be called?
This sounds suspiciously like formatting for display or processing user input or something like that rather than an actual constraint you want to impose on strings in general. In that case, an NSFormatter class or a bit of code in some specific controller property setter would be a good idea.
But if you really want this to be on the string itself, you either need to provide a method like stringByTruncatingToLength: on NSString or you need to switch to using NSMutableString everywhere, because NSStrings cannot be modified at all and thus a setMaximumLength: method would be kind of meaningless.
The mechanism of truncation can be implemented like this:(for a NSString property of a custom class)
-(void)setName:(NSString*)newName
{
if([newName length]> maxLength)
{
newName = [newName substringWithRange:NSMakeRange([newName length] - maxLength, maxLength];
}
_name = newName;
}

Objective C - Override setter to accept objects of a different type

I'm trying to override the setter of an NSManagedObject so that I can pass in an object of a different type, do a transformation and then set the property. Something like this:
- (void)setContentData:(NSData *)contentData
{
NSString *base64String;
// do some stuff to convert data to base64-encoded string
// ...
[self willChangeValueForKey:#"contentData"];
[self setPrimitiveValue:base64String forKey:#"contentData"];
[self didChangeValueForKey:#"contentData"];
}
So, in this case the contentData field of my NSManagedObject is an NSString *, and I want to allow the setter to accept an NSData * which I would then convert to an NSString * and save it to the model. However, if I try to do this I get warnings from the compiler about trying to assign an NSData * to an NSString *:
myObject.contentData = someNSData;
-> Incompatible pointer types assigning to 'NSString *' from 'NSData *__strong'
Is there a better way to go about this, or perhaps I should avoid the setters altogether and create custom "setters" that allow me to pass in the NSData * and set the NSString * field without a compiler warning?
I think this is an instance where your fighting with the tools and frameworks is a significant design smell. Retreat from this notion of trying to override the expected data type of a fundamental property for your class.
You didn't say whether the NSManagedObject you are subclassing is under your control. If it's going to be part of your design to have it be something of a template for management of other types of contentData than NSString, then declare it as type id in the root class and specialize in the subclasses. That should prevent the warning.
Probably, you want to follow a Cocoaism: don't subclass. Can you achieve whatever functionality you're looking for from the superclass by say extracting it into a helper class that is held as a property by each of the varying-behavior managed object classes?
following up on my "setContentData: (id) contentData" comment, try something like this:
- (void)setContentData:(id)thingToWorkWith
{
NSString * base64String = nil;
if(thingToWorkWith isKindOfClass: [NSData class])
{
// convert data to string
}
if(thingToWorkWith isKindOfClass: [NSString class])
{
// set up base64 string properly
}
if(base64String)
{
// do some stuff to convert data to base64-encoded string
// ...
[self willChangeValueForKey:#"contentData"];
[self setPrimitiveValue:base64String forKey:#"contentData"];
[self didChangeValueForKey:#"contentData"];
}
}
Make sure to get rid of the "#synthesize" bit for contentData in your .m file, create a "getter" method as well, and because you're using "id" for the setter parameter, you may have to adjust your "#property" declaration a bit. I haven't tried exactly what you are attempting to do (i.e. no warranties on this technique).

Cocoa: deserialize json string to custom objects (not NSDictionary, NSArray)

In java-land, there are a handful of useful libraries which will convert json strings to objects of matching type. The json libraries I've seen for cocoa simply create nested NSDictionaries and NSArrays. Is there a tool out there which will go the extra step of reconstituting whatever object type I want?
So, for example, if I have a class called "Unicorn", with a property "maneColor", and I have json that looks like this:
{
"maneColor":"silver"
}
I can automatically instantiate a Unicorn object with "maneColor" set to "silver".
I'm not aware of any specific implementations, but key-value coding gets you very close to what you want: Key Value Coding Guide. I've had good results combining streamed json parsing with KVC.
The -setValue:forKey: method makes adapting serialized data to custom objects fairly straightforward. To continue with your example, you'd create a Unicorn class with all required accessor methods: -setName:/-name, -setManeColor/-maneColor, etc. (You may be able to use properties for some expected values, but there are cases, as with the maneColor value, where you probably want to write a custom setter to convert from the color name string to an NSColor or UIColor object.)
You'll also want to add two more methods to your custom object: -setValue:forUndefinedKey: and -valueForUndefinedKey:. These are the methods that will be called if your object has no accessor methods matching a key passed into the KVC methods. You can catch unexpected or unsupported values here, and store them or ignore them as necessary.
When you send -setValue:forKey: to the Unicorn object, the framework looks for accessors matching the key pattern. For instance, if the key is "maneColor" and you're setting the value, the framework checks to see if your object implements -setManeColor:. If so, it invokes that method, passing in the value; otherwise, -setValue:forUndefinedKey: is called, and if your object doesn't implement it, an exception is thrown.
When your parser's delegate receives notification that parsing a json unicorn object has begun, instantiate a Unicorn object. As your parser returns the parsed data to you, use -setValue:forKey: to add the data to your object:
- ( void )parserDidBeginParsingDictionary: (SomeParser *)p
{
self.currentUnicorn = [ Unicorn unicorn ];
}
- ( void )parser: (SomeParser *)p didParseString: (NSString *)string
forKey: (NSString *)key
{
[ self.currentUnicorn setValue: string forKey: key ]
}
- ( void )parserDidFinishParsingDictionary: (SomeParser *)p
{
[ self.unicorns addObject: self.currentUnicorn ];
}
Use Jastor - https://github.com/elado/jastor
Takes already parsed JSON into NSDictionary and fills an instance of real Objective-C class.
NSDictionary *parsedJSON = (yajl, JSONKit etc)
Unicorn *unicorn = [[Unicorn alloc] initWithDictionary:parsedJSON];
unicorn.maneColor // "silver"
As any subclass of NSObject conforms to NSKeyValueCoding protocol:
NSDictionary *parsedJSON = //whatever
id <NSKeyValueCoding> entity = [[CustomNSObjectSubclass alloc] init];
[entity setValuesForKeysWithDictionary:parsedJSON];
Apple added the NSJSONSerialization class to iOS 5.0 which, according to the documentation, does the following:
You use the NSJSONSerialization class to convert JSON to Foundation
objects and convert Foundation objects to JSON.
An object that may be converted to JSON must have the following
properties:
The top level object is an NSArray or NSDictionary. All objects are
instances of NSString, NSNumber, NSArray, NSDictionary, or NSNull. All
dictionary keys are instances of NSString. Numbers are not NaN or
infinity.
Here's a tutorial and wrapper method to get you started.