Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 832
Ser Pounce Avatar asked Dec 18 '13 02:12

Ser Pounce


2 Answers

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.

like image 60
Tom Harrington Avatar answered Sep 27 '22 22:09

Tom Harrington


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

like image 29
LaborEtArs Avatar answered Sep 27 '22 22:09

LaborEtArs