Saving custom attributes in NSAttributedString

I need to add a custom attribute to the selected text in an NSTextView. So I can do that by getting the attributed string for the selection, adding a custom attribute to it, and then replacing the selection with my new attributed string.

So now I get the text view’s attributed string as NSData and write it to a file. Later when I open that file and restore it to the text view my custom attributes are gone! After working out the entire scheme for my custom attribute I find that custom attributes are not saved for you. Look at the IMPORTANT note here:

  • How to securely include secret key/signature in iOS/Cocoa apps
  • Cocoa Storyboard Responder Chain
  • Enable LLVM + Clang in Xcode new project causes linking errors
  • How to view contents of NSDictionary variable in Xcode debugger?
  • What is the right choice between NSDecimal, NSDecimalNumber, CFNumber?
  • What does Apple mean when they say that a NSManagedObjectContext is owned by the thread or queue that created it?
  • So I have no idea how to save and restore my documents with this custom attribute. Any help?

    2 Solutions Collect From Internet About “Saving custom attributes in NSAttributedString”

    The normal way of saving an NSAttributedString is to use RTF, and RTF data is what the -dataFromRange:documentAttributes:error: method of NSAttributedString generates.

    However, the RTF format has no support for custom attributes. Instead, you should use the NSCoding protocol to archive your attributed string, which will preserve the custom attributes:

    //asssume attributedString is your NSAttributedString
    //encode the string as NSData
    NSData* stringData = [NSKeyedArchiver archivedDataWithRootObject:attributedString];
    [stringData writeToFile:pathToFile atomically:YES];
    //read the data back in and decode the string
    NSData* newStringData = [NSData dataWithContentsOfFile:pathToFile];
    NSAttributedString* newString = [NSKeyedUnarchiver unarchiveObjectWithData:newStringData];

    There is a way to save custom attributes to RTF using Cocoa. It relies on the fact that RTF is a text format, and so can be manipulated as a string even if you don’t know all the rules of RTF and don’t have a custom RTF reader/writer. The procedure I outline below post-processes the RTF both when writing and reading, and I have used this technique personally. One thing to be very careful of is that the text you insert into the RTF uses only 7-bit ASCII and no unescaped control characters, which include “\ { }”.

    Here’s how you would encode your data:

    NSData *GetRtfFromAttributedString(NSAttributedString *text)
        NSData *rtfData = nil;
        NSMutableString *rtfString = nil;
        NSString *customData = nil, *encodedData = nil;
        NSRange range;
        NSUInteger dataLocation;
    // Convert the attributed string to RTF
        if ((rtfData = [text RTFFromRange:NSMakeRange(0, [text length]) documentAttributes:nil]) == nil)
    // Find and encode your custom attributes here. In this example the data is a string and there's at most one of them
        if ((customData = [text attribute:@"MyCustomData" atIndex:0 effectiveRange:&range]) == nil)
            return(rtfData); // No custom data, return RTF as is
        dataLocation = range.location;
    // Get a string representation of the RTF
        rtfString = [[NSMutableString alloc] initWithData:rtfData encoding:NSASCIIStringEncoding];
    // Find the anchor where we'll put our data, namely just before the first paragraph property reset
        range = [rtfString rangeOfString:@"\\pard" options:NSLiteralSearch];
        if (range.location == NSNotFound)
            NSLog(@"Custom data dropped; RTF has no paragraph properties");
            [rtfString release];
    // Insert the starred group containing the custom data and its location
        encodedData = [NSString stringWithFormat:@"{\\*\\my_custom_keyword %d,%@}\n", dataLocation, customData];
        [rtfString insertString:encodedData atIndex:range.location];
    // Convert the amended RTF back to a data object    
        rtfData = [rtfString dataUsingEncoding:NSASCIIStringEncoding];
        [rtfString release];

    This technique works because all compliant RTF readers will ignore “starred groups” whose keyword they don’t recognize. Therefore you want to be sure your control word will not be recognized by any other reader, so use something likely to be unique, such as a prefix with your company or product name. If your data is complex, or binary, or may contain illegal RTF characters that you don’t want to escape, encode it in base64. Be sure you put a space after your keyword.

    Similarly, when reading the RTF, you search for your control word, extract the data, and restore the attribute. This routine takes as arguments the attributed string and the RTF it was created from.

    void RestoreCustomAttributes(NSMutableAttributedString *text, NSData *rtfData)
        NSString *rtfString = [[NSString alloc] initWithData:rtfData encoding:NSASCIIStringEncoding];
        NSArray *components = nil;
        NSRange range, endRange;
    // Find the custom data and its end
        range = [rtfString rangeOfString:@"{\\*\\my_custom_keyword " options:NSLiteralSearch];
        if (range.location == NSNotFound)
            [rtfString release];
        range.location += range.length;
        endRange = [rtfString rangeOfString:@"}" options:NSLiteralSearch
            range:NSMakeRange(range.location, [rtfString length] - endRange.location)];
        if (endRange.location == NSNotFound)
            [rtfString release];
    // Get the location and the string data, which are separated by a comma
        range.length = endRange.location - range.location;
        components = [[rtfString substringWithRange:range] componentsSeparatedByString:@","];
        [rtfString release];
    // Assign the custom data back to the attributed string. You should do range checking here (omitted for clarity)
        [text addAttribute:@"MyCustomData" value:[components objectAtIndex:1]
            range:NSMakeRange([[components objectAtIndex:0] integerValue], 1)];