Chris
Chris

Reputation: 1013

Encoding NSAttributedString Throws Error

Based on the accepted answer to this question I wrote the following code:

NSData* somedata;
somedata=[NSKeyedArchiver archivedDataWithRootObject:ts];

where ts is an NSAttributedString that is populated with some text and some attributes (colors, in this case).

When I execute this code, I receive this error:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFType encodeWithCoder:]: unrecognized selector sent to instance 0x6eb5b90'

I'm new to the NSCoder arena, but the answer to the aforementioned question made it seem like this is all I have to do. Is it? Did I miss something?


EDIT:

The unrecognized selector in this case is being sent to a color attribute in the NSAttributedString. When I initialize the string like so:

NSAttributedString *ts = [[NSAttributedString alloc] initWithString:text attributes:self.currentAttributeDictionary];

The dictionary is built like so:

self.currentAttributeDictionary=[NSDictionary dictionaryWithObjectsAndKeys:
                                 [self.currentColor CGColor],(NSString*)kCTForegroundColorAttributeName,
                                 nil];

And an NSLog of the dictionary yields this:

New dictionary is: {
CTForegroundColor = "<CGColor 0x6eb5b90> [<CGColorSpace 0x6e968c0> (kCGColorSpaceDeviceRGB)] ( 1 1 0 1 )";}

The address of the CGColor, above, matches the address in the error message.

Upvotes: 0

Views: 3420

Answers (2)

Chris
Chris

Reputation: 1013

As requested, here's the code I used to accomplish what i needed to accomplish. It's been a year since I looked at this code, and it was written more to understand what was going on than for great coding practices or for any sort of efficiency. However, it did work, and it worked great!

I defined a category of NSAttributedString code is below.

Example use:

-(void)code:(id)sender {    
    self.testData=[textView.attributedString customEncode];
    NSLog(@"%@",self.testData);
}

-(void)recover:(id)sender {
    NSAttributedString* tString=[NSMutableAttributedString customDecode:self.testData];
    NSLog(@"Recover pressed: %@",tString);
    textView.attributedString=tString;
}

And here's the underlying code:

#import "NSAttributedString+Extras.h"
#import <CoreText/CoreText.h>

@implementation NSAttributedString (Extras)

-(NSData*)customEncode {
    __block NSMutableArray* archivableAttributes=[[NSMutableArray alloc]init];

    [self enumerateAttributesInRange:NSMakeRange(0, [self length]) options:0 usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
        NSLog(@"range: %d %d",range.location, range.length);
        NSLog(@"dict: %@",attrs);
        NSLog(@"keys: %@", [attrs allKeys]);
        NSLog(@"values: %@", [attrs allValues]);

        NSMutableDictionary* tDict=[[NSMutableDictionary alloc]init];

        [tDict setObject:[NSNumber numberWithInt:range.location] forKey:@"location"];
        [tDict setObject:[NSNumber numberWithInt:range.length] forKey:@"length"];

        for (NSString* tKey in [attrs allKeys]) {
            if ([tKey isEqualToString:@"CTUnderlineColor"]) {
                [tDict setObject:[NSAttributedString arrayFromCGColorComponents:((CGColorRef)[attrs objectForKey:@"CTUnderlineColor"])] forKey:@"CTUnderlineColor"];
            }
            if ([tKey isEqualToString:@"NSUnderline"]) {
                NSNumber* underline=[attrs objectForKey:@"NSUnderline"];
                [tDict setObject:underline forKey:@"NSUnderline"];
            }
            if ([tKey isEqualToString:@"CTForegroundColor"]) {
                [tDict setObject:[NSAttributedString arrayFromCGColorComponents:((CGColorRef)[attrs objectForKey:@"CTForegroundColor"])] forKey:@"CTForegroundColor"];
            }
            if ([tKey isEqualToString:@"NSFont"]) {
                CTFontRef font=((CTFontRef)[attrs objectForKey:@"NSFont"]);

                NSDictionary* fontDict=[NSDictionary 
                                        dictionaryWithObjects:
                                        [NSArray arrayWithObjects:(NSString*)CTFontCopyPostScriptName(font),[NSNumber numberWithFloat:CTFontGetSize(font)], nil]
                                        forKeys:
                                        [NSArray arrayWithObjects:@"fontName", @"fontSize", nil]];

                [tDict setObject:fontDict forKey:@"NSFont"];
            }
        }

        [archivableAttributes addObject:tDict];
    }];

    NSMutableDictionary* archiveNSMString=[NSMutableDictionary 
                                           dictionaryWithObjects: [NSArray arrayWithObjects:[self string],archivableAttributes,nil]
                                           forKeys:[NSArray arrayWithObjects:@"string",@"attributes",nil]];

    NSLog(@"archivableAttributes array: %@",archiveNSMString);

    NSData* tData=[NSKeyedArchiver archivedDataWithRootObject:archiveNSMString];

    NSLog(@"tdata: %@",tData);

    return tData;
}

+(NSAttributedString*)customDecode:(NSData *)data {
    NSMutableAttributedString* tString;
    NSMutableDictionary* tDict=[NSKeyedUnarchiver unarchiveObjectWithData:data];
    NSArray* attrs;

    CTFontRef font=NULL;
    CGColorRef color=NULL;
    NSNumber* underlineProp=[NSNumber numberWithInt:0];
    CGColorRef underlineColor=NULL;

    NSLog(@"decoded dictionary: %@",tDict);

    if ([[tDict allKeys]containsObject:@"string"]) {
        tString=[[NSMutableAttributedString alloc]initWithString:((NSString*)[tDict objectForKey:@"string"])];
    }
    else {
        tString=[[NSMutableAttributedString alloc]initWithString:@""];
    }

    if ([[tDict allKeys]containsObject:@"attributes"]) {
        attrs=[tDict objectForKey:@"attributes"];
    }
    else {
        attrs=nil;
    }

    for (NSDictionary* attDict in attrs) {
        int location=-1;
        int length=-1;
        NSRange insertRange=NSMakeRange(-1, 0);

        if ([[attDict allKeys]containsObject:@"location"]) {
            location=[[attDict objectForKey:@"location"]intValue];
        }
        if ([[attDict allKeys]containsObject:@"length"]) {
            length=[[attDict objectForKey:@"length"]intValue];
        }
        if (location!=-1&&length!=-1) {
            insertRange=NSMakeRange(location, length);
        }

        if ([[attDict allKeys]containsObject:@"NSUnderline"]) {
            underlineProp=[attDict objectForKey:@"NSUnderline"];
        }

        if ([[attDict allKeys]containsObject:@"CTUnderlineColor"]) {
            underlineColor=[NSAttributedString cgColorRefFromArray:[attDict objectForKey:@"CTUnderlineColor"]];
        }        

        if ([[attDict allKeys]containsObject:@"CTForegroundColor"]) {
            color=[NSAttributedString cgColorRefFromArray:[attDict objectForKey:@"CTForegroundColor"]];
        }

        if ([[attDict allKeys]containsObject:@"NSFont"]) {
            NSString* name=nil;
            float size=-1;

            NSDictionary* fontDict=[attDict objectForKey:@"NSFont"];

            if ([[fontDict allKeys]containsObject:@"fontName"]) {
                name=[fontDict objectForKey:@"fontName"];
            }
            if ([[fontDict allKeys]containsObject:@"fontSize"]) {
                size=[[fontDict objectForKey:@"fontSize"]floatValue];
            }

            if (name!=nil&&size!=-1) {
                font=CTFontCreateWithName((CFStringRef)name, size, NULL);
            }
        }

        if (insertRange.location!=-1) {
            if (color!=NULL) {
                [tString addAttribute:(NSString*)kCTForegroundColorAttributeName value:(id)color range:insertRange];
            }
            if (font!=NULL) {
                [tString addAttribute:(NSString*)kCTFontAttributeName value:(id)font range:insertRange];
            }
            if ([underlineProp intValue]!=0&&underlineColor!=NULL) {
                [tString addAttribute:(NSString*)kCTUnderlineColorAttributeName value:(id)underlineColor range:insertRange];
                [tString addAttribute:(NSString*)kCTUnderlineStyleAttributeName value:(id)underlineProp range:insertRange];
            }
       }
    } 

    [tString enumerateAttributesInRange:NSMakeRange(0, [tString length]) options:0 usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) {
        NSLog(@"range: %d %d",range.location, range.length);
        NSLog(@"dict: %@",attrs);
        NSLog(@"keys: %@", [attrs allKeys]);
        NSLog(@"values: %@", [attrs allValues]);
    }];

    return [[NSAttributedString alloc]initWithAttributedString:tString];
}

+(NSArray*)arrayFromCGColorComponents:(CGColorRef)color {
    int numComponents=CGColorGetNumberOfComponents(color);
    CGFloat* components=CGColorGetComponents(color);
    NSMutableArray* retval=[[NSMutableArray alloc]init];
    for(int i=0;i<numComponents;i++) {
        [retval addObject:[NSNumber numberWithFloat:components[i]]];
    }
    return [NSArray arrayWithArray:retval];
}

+(CGColorRef)cgColorRefFromArray:(NSArray*)theArray {
    CGFloat* array=malloc(sizeof(CGFloat)*[theArray count]);
    for (int i=0; i<[theArray count]; i++) {
        array[i]=[[theArray objectAtIndex:i]floatValue];
    }

    CGColorSpaceRef theSpace;

    if ([theArray count]==2) {
        theSpace=CGColorSpaceCreateDeviceGray();
    }
    else {
        theSpace=CGColorSpaceCreateDeviceRGB();
    }

    return CGColorCreate(theSpace, array);
}

@end

Upvotes: 0

Conrad Shultz
Conrad Shultz

Reputation: 8808

While UIColor conforms to NSCoding, it is (unlike most such classes) not toll-free bridged to CGColorRef. Your dictionary is attempting to encode its contents, and CGColorRef doesn't know how to encode itself.

Presuming that you don't want to encode a UIColor instead (since these sound like Core Text attributes), you are going to have to handle serialization of the CGColorRef yourself. See, for example, this question for some useful thoughts.

It should be noted, btw, since I don't know where the archived data is going, that if you want to unarchive the data on OS X that colors again become a headache at the AppKit/UIKit level: NSColor and UIColor are not directly compatible, so you would still need to go via CGColorRef, stashing the color space information as appropriate.

Upvotes: 6

Related Questions