How do I implement NSAttributedStringValueTransformer for attributed strings stored in Core Data

I have an application that uses a Transformable property in Core Data to store NSAttributedStrings and get compiler warnings

Code Block
Object.property is using a nil or insecure value transformer. Please switch to NSSecureUnarchiveFromDataTransformerName or a custom NSValueTransformer subclass of NSSecureUnarchiveFromDataTransformer [2]


NSSecureUnarchiveFromDataTransformerName does not support archiving and unarchiving of NSAttributedStrings and so as I understand it I have to create a custom transformer, register that in AppDelegate and enter the transformer class name in the Core Data models object property details.

Below is the custom transformer class, however I get an error when trying to decode existing attributed strings. Can anyone shed any light on this ? Why is the new unarchiver unable to handle the NSFileWrapper given this is a property of the NSTextAttachment and works fine with the deprecated unarchiver ?

Is this a bug or intentional ?

Is there some way to add support for unarchiving the NSFileWrapper ?

Code Block
@implementation NSAttributedStringValueTransformer
+(Class)transformedValueClass {
return [NSAttributedString class];
}
+(void)initialize {
[NSValueTransformer setValueTransformer:[[self alloc]init] forName:@"NSAttributedStringValueTransformer"];
}
+(BOOL)allowsReverseTransformation {
return YES;
}
-(NSData*)transformedValue:(NSAttributedString*)value {
NSError *error;
NSData* stringAsData = [NSKeyedArchiver archivedDataWithRootObject:value requiringSecureCoding:false error:&error];
if (error != nil) {
NSLog(@"Error encoding attributed string: %@", error.localizedDescription);
return nil;
}
return stringAsData;
}
-(NSAttributedString*)reverseTransformedValue:(NSData*)value {
NSError *error;
/* This works */
/* NSAttributedString* string = [NSKeyedUnarchiver unarchiveObjectWithData: value];
*/
/* This fails with the error The data couldn’t be read because it isn’t in the correct format. */
NSAttributedString* string = [NSKeyedUnarchiver unarchivedObjectOfClass:[NSAttributedString class] fromData:value error:&error];
if (error != nil) {
NSLog(@"Error decoding attributed string: %@", error);
return nil;
}
return string;
}
@end


Resulting Error:

Code Block
Error decoding attributed string:
[Error](https://developer.apple.com/forums/content/attachment/547d2a08-9220-42aa-9d29-1b3129feb864){: .log-attachment}
Error Domain=NSCocoaErrorDomain Code=4864 "value for key 'NSFileWrapper' was of unexpected class 'NSFileWrapper (0x1c9d40d48) [/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework]'. Allowed classes are '{(
"NSURL (0x1c9d1b988) [/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework]",
"NSAttributedString (0x1c9d36668) [/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/Foundation.framework]",
"NSFont (0x1c9e1a3c8) [/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIFoundation.framework]",
"NSDictionary (0x1c9d1adf8) [/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework]",
"NSArray (0x1c9d1ab28) [/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/Frameworks/CoreFoundation.framework]",
"NSColor (0x1c9ec2198) [/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIKitCore.framework]",
"NSTextAttachment (0x1c9e1aad0) [/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIFoundation.framework]",
"NSGlyphInfo (0x1c9e198d8) [/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/UIFoundation.framework]",
...

OK, just to make sure we’re working from the same starting point, here’s a tiny snippet that reproduces the problem:

Code Block
let fu = URL(fileURLWithPath: "/Users/quinn/Test/test.txt")
let f = try FileWrapper(url: fu, options: [.immediate])
let a = NSTextAttachment(fileWrapper: f)
// let a = NSTextAttachment(data: Data("an attached text document".utf8), ofType: "public.utf8-plain-text")
let s = NSMutableAttributedString(string: "Hello Cruel World!")
s.setAttributes([.attachment: a], range: NSRange(location: 0, length: 1))
let d = try NSKeyedArchiver.archivedData(withRootObject: s, requiringSecureCoding: true)
let us = try NSKeyedUnarchiver.unarchivedObject(ofClass: NSAttributedString.self, from: d)!
// The above throws `value for key 'NSFileWrapper' was of unexpected class 'NSFileWrapper …'. Allowed classes are…`.
print(us)


Note that the problem does not reproduce if I create the text attachment from data rather than a file wrapper, that is, if I comment out line 1 through 3 and uncomment line 4.

Looking at the source for this error I think it’s a straightforward bug in NSTextAttachment. When it goes to decode the NSFileWrapper it uses the old school decode method (-decodeObjectForKey:) rather than the more modern secure coding version (-decodeObjectOfClass:forKey:). I encourage you to file a bug about this. Please post your bug number, just for the record.

As to workarounds, the obvious one is to use a text attachment created from data rather than a file wrapper. However, that only works for newly-created archives. Do you have existing archives that you need to decode?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Thanks, I figured that it might be a bug and have submitted a report via FeedbackAssistant (#FB9079551) and raised a DTI where they told me you had responded here :-)

Yes I have existing application with existing archives. Note that the attachments are PDF documents or images the user may have pasted in to the text view.

Not sure whether it would be possible to convert the file attachments to data without loosing some file details but I suspect so.

Anyway thanks for the great response. I don't think I have ever had a useful response from anyone from Apple on these forums before - great to see such good support these days. ***** (that's 5 stars!)

Regards

Yes I have existing application with existing archives.

Tricky. I don’t have any immediate suggestions on that front so, if you need help reading your existing archives, my advice is that you respond to DTS’s email saying that, yes, you would like help with a workaround.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
How do I implement NSAttributedStringValueTransformer for attributed strings stored in Core Data
 
 
Q