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: http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/AttributedStrings/Tasks/RTFAndAttrStrings.html
So I have no idea how to save and restore my documents with this custom attribute. Any help?
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)
return(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];
return(rtfData);
}
// 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];
return(rtfData);
}
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];
return;
}
range.location += range.length;
endRange = [rtfString rangeOfString:@"}" options:NSLiteralSearch
range:NSMakeRange(range.location, [rtfString length] - endRange.location)];
if (endRange.location == NSNotFound)
{
[rtfString release];
return;
}
// 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)];
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With