Ser Pounce
Ser Pounce

Reputation: 14527

Simple way to store NSMutableAttributedString in CoreData

I'm trying to store an NSMutableAttributedString in CoreData, but am running into problems since some of the attributes of my NSMutableAttributedString contain Core Foundation objects that can't be archived. Is there an easy way to get this object to store in CoreData without having to do some messy stuff manually?

Upvotes: 5

Views: 2767

Answers (4)

LaborEtArs
LaborEtArs

Reputation: 2033

While the above answer is right, it has one big disadvantage:

It is not possible to build a fetch request / predicate that queries the content of the NSAttributedString object!

A predicate like this will cause an exception when executed:

[NSPredicate predicateWithFormat:@"(content CONTAINS[cd] %@)", @"test"]];

To store an 'fetchable' NSAttributedString in Core Data is is needed to spilt the NSAttributedString into two parts: A NSString side (which can be fetched) and a NSData side, which stores the attributes.

This split can be achieved by creating three attributes in the Core Data entity:

  1. a shadow NSString attribute ('contentString')
  2. a shadow NSData attribute ('contentAttributes')
  3. an 'undefined' transient attributed ('content')

In the custom entities class the 'content' attributed the created from its shadows and changes to the attribute are also mirrored to its shadows.

Header file:

/**
 MMTopic
*/
@interface MMTopic : _MMTopic {}

@property (strong, nonatomic) NSAttributedString*   content;

@end

Implementation file:

/**
MMTopic (PrimitiveAccessors)

*/

@interface MMTopic (PrimitiveAccessors)

- (NSAttributedString *)primitiveContent;
- (void)setPrimitiveContent:(NSAttributedString *)pContent;

@end


/**
 MMTopic

 */
@implementation MMTopic    

static NSString const*  kAttributesDictionaryKey =  @"AttributesDictionary";
static NSString const*  kAttributesRangeKey =       @"AttributesRange";

/*
 awakeFromFetch

 */
- (void)awakeFromFetch {

    [super awakeFromFetch];

    // Build 'content' from its shadows 'contentString' and 'contentAttributes'
    NSString*                   string = self.contentString;
    NSMutableAttributedString*  content = [[NSMutableAttributedString alloc] initWithString:string];

    NSData*                     attributesData = self.contentAttributes;
    NSArray*                    attributesArray = nil;
    if (attributesData) {
        NSKeyedUnarchiver*  decoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:attributesData];
        attributesArray = [[NSArray alloc] initWithCoder:decoder];
    }

    if ((content) &&
        (attributesArray.count)) {

        for (NSDictionary* attributesDictionary in attributesArray) {
            //NSLog(@"%@: %@", NSStringFromRange(((NSValue*)attributesDictionary[kAttributesRangeKey]).rangeValue), attributesDictionary[kAttributesDictionaryKey]);
            [content addAttributes:attributesDictionary[kAttributesDictionaryKey]
                             range:((NSValue*)attributesDictionary[kAttributesRangeKey]).rangeValue];
        }

        [self setPrimitiveContent:content];
    }
}

/*
 content

 */
@dynamic content;

/*
 content (getter)

 */
- (NSAttributedString *)content {

    [self willAccessValueForKey:@"content"];
    NSAttributedString* content = [self primitiveContent];
    [self didAccessValueForKey:@"content"];

    return content;
}

/*
 content (setter)

 */
- (void)setContent:(NSAttributedString *)pContent {

    [self willChangeValueForKey:@"content"];
    [self setPrimitiveValue:pContent forKey:@"content"];
    [self didChangeValueForKey:@"content"];

    // Update the shadows
    // contentString
    [self setValue:pContent.string
            forKey:@"contentString"];

    // contentAttributes
    NSMutableData*      data = [NSMutableData data];
    NSKeyedArchiver*    coder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
    NSMutableArray*     attributesArray = [NSMutableArray array];
    [pContent enumerateAttributesInRange:NSMakeRange(0, pContent.length)
                                 options:0
                              usingBlock:^(NSDictionary* pAttributesDictionary, NSRange pRange, BOOL* prStop) {
                                  //NSLog(@"%@: %@", NSStringFromRange(pRange), pAttributesDictionary);
                                  [attributesArray addObject:@{
                                                               kAttributesDictionaryKey:    pAttributesDictionary,
                                                               kAttributesRangeKey:     [NSValue valueWithRange:pRange],
                                                               }];
                              }];
    [attributesArray encodeWithCoder:coder];
    [coder finishEncoding];

    [self setValue:data
            forKey:@"contentAttributes"];
}

@end

Fetching can now be done by:

[NSPredicate predicateWithFormat:@"(contentString CONTAINS[cd] %@)", @"test"]];

While any access to the NSAttributedString goes like this:

textField.attributedText = pTopic.content;

The rules for working with 'Non-Standard attributes' in Core Data are documented here: Apple docs

Upvotes: 5

Ser Pounce
Ser Pounce

Reputation: 14527

I started using CoreText when iOS5 was out, and thus used the Core Foundation values as attributes. However I now realize that since iOS6 came out, I can now use NSForegroundColorAttributeName, NSParagraphStyleAttributeName, NSFontAttributeName, etc. in the attributes dictionary, and those keys are accompanied by objects like UIColor, NSMutableParagraphStyle, and UIFont which can be archived.

Upvotes: 1

Duncan Groenewald
Duncan Groenewald

Reputation: 8988

Well I am not sure what you are trying to do with the attributed string, but if it's formatted text then can't you use NSFont, etc..

Take a look here http://ossh.com.au/design-and-technology/software-development, I posted some stuff on formatting styles and images with uitextview and nstextview, but mostly it's about attributed strings.

This stuff is all stored in core data.

Upvotes: 1

Tom Harrington
Tom Harrington

Reputation: 70946

NSMutableAttributedString conforms to NSCoding, which means that it knows how to convert itself to/from an NSData and does so via a protocol that Core Data knows how to use.

Make the attribute "transformable", and then just assign attributed strings to it. Since it's transformable, Core Data will use NSCoding to convert it to NSData when you assign a value, and to convert it back to an attributed string when you read it.

Note, you won't be able to use a predicate to filter results on this field. But storing and retrieving it is simple.

Upvotes: 5

Related Questions