Simple way to store NSMutableAttributedString in CoreData - ios

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?

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.

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:
a shadow NSString attribute ('contentString')
a shadow NSData attribute ('contentAttributes')
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

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.

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.

Related

UIPasteBoard "string" property returning nil despite "hasStrings" being true

I have the following code I use to grab text that a user has copied to the clipboard from outside the app so they can paste it in within the app:
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
if ([pasteboard hasStrings])
{
NSString *text = pasteboard.string;
}
This has always worked fine, until iOS14 I notice I've been getting crashes because pasteboard.string is nil despite hasStrings being true.
I looked into the documentation and discovered that indeed, it's possible for pasteboard.string to be nil:
The value stored in this property is an NSString object. The
associated array of representation types is
UIPasteboardTypeListString, which includes type kUTTypeUTF8PlainText.
Setting this property replaces all current items in the pasteboard
with the new item. If the first item has no value of the indicated
type, nil is returned.
I take this mean to that some sort of string that is not kUTTypeUTF8PlainText is in the clipboard and that's why pasteboard.string is nil, but is this the correct interpretation?
I'm just confused as to what exactly is happening here and am unsure what to tell my user if I reach the case where pasteboard.string is nil?
-[UIPasteboard hasStrings] == YES only means the items in pasteboard have type of public.utf8-plain-text or any other types that indicates it's a string.
But -[UIPasteboard string] can still return nil if object of class NSString cannot be constructed from any data provided by itemProviders.
Here is an example to reproduce the situation you are in:
First implement a test class that conforms to NSItemProviderWriting
#import <Foundation/Foundation.h>
static NSString *const UTTypeUTF8PlainText = #"public.utf8-plain-text";
#interface TestObject : NSObject <NSItemProviderWriting>
#end
#implementation TestObject
- (NSData *)randomDataWithLength:(NSUInteger)length {
NSMutableData *data = [NSMutableData dataWithLength:length];
SecRandomCopyBytes(kSecRandomDefault, length, data.mutableBytes);
return data;
}
#pragma mark - NSItemProviderWriting
+ (NSArray<NSString *> *)writableTypeIdentifiersForItemProvider {
return #[UTTypeUTF8PlainText];
}
- (nullable NSProgress *)loadDataWithTypeIdentifier:(nonnull NSString *)typeIdentifier forItemProviderCompletionHandler:(nonnull void (^)(NSData * _Nullable, NSError * _Nullable))completionHandler {
// random data that an utf8 string may not be constructed from
NSData *randomData = [self randomDataWithLength:1];
completionHandler(randomData, nil);
return nil;
}
#end
Then put the test object into pastboard
if (#available(iOS 11.0, *)) {
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
TestObject *item = [TestObject new];
[pasteboard setObjects:#[item]];
if ([pasteboard hasStrings]) {
// text may be nil
NSString *text = pasteboard.string;
}
}

Appending data to a string without losing previous data

I have this is on the top of my program:
#property (strong, nonatomic) NSMutableData *data;
I thought this would allow me to store the value from every time this method runs:
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray *)requests
{
for (CBATTRequest *request in requests) {
NSString *stringValue = [[NSString alloc] initWithData: [request value] encoding:NSUTF8StringEncoding];
// Have we got everything we need?
if ([stringValue isEqualToString:#"EOM"]) {
// We have, so show the data,
[self.textview setText:[[NSString alloc] initWithData:self.data encoding:NSUTF8StringEncoding]];
}
// Otherwise, just add the data on to what we already have
[self.data appendData:[request value]];
}
}
This method waits for a write request to be received and stores the value in a string. I have a core bluetooth central that is sending three blocks of data. This is to get around the data transfer size restriction within bluetooth LE. The problem is I can't get the three values stored. I am trying to not just store the last value but add the new value to the end of a nsstring or nssdata every time the method is called. Any help would be greatly appreciated. I thought the property at the top would allow me to do it but it either only stores the last value or nothing at all. I am not used to the ways of objective c yet. Thanks.
Even this doesn't write anything to self.data:
NSString * result = [[requests valueForKey:#"value"] componentsJoinedByString:#""];
NSData* data = [result dataUsingEncoding:NSUTF8StringEncoding];
[self.data appendData:data];
// Log it
NSLog(#"%#",self.data);
You should use NSMutableArray instead of NSString as a mutable string.
- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray *)requests
{
NSMutableString *stringValue = [[NSMutableString alloc] init];
for (CBATTRequest *request in requests) {
[stringValue appendString:[[NSString alloc] initWithData:[request value] encoding:NSUTF8StringEncoding]];
// Have we got everything we need?
if ([stringValue isEqualToString:#"EOM"]) {
// We have, so show the data,
[self.textview setText:[[NSString alloc] initWithData:self.data encoding:NSUTF8StringEncoding]];
}
// Otherwise, just add the data on to what we already have
[self.data appendData:[request value]];
}
Remember kids when dealing with NSMutableData always initialize it!
_data = [[NSMutableData alloc] init];
This fixed the null problem for me.

Copy NSAttributedString in UIPasteBoard

How do you copy an NSAttributedString in the pasteboard, to allow the user to paste, or to paste programmatically (with - (void)paste:(id)sender, from UIResponderStandardEditActions protocol).
I tried:
UIPasteboard *pasteBoard = [UIPasteboard generalPasteboard];
[pasteBoard setValue:attributedString forPasteboardType:(NSString *)kUTTypeRTF];
but this crash with:
-[UIPasteboard setValue:forPasteboardType:]: value is not a valid property list type'
which is to be expected, because NSAttributedString is not a property list value.
If the user paste the content of the pasteboard in my app, I would like to keep all the standards and custom attributes of the attributed string.
I have found that when I (as a user of the application) copy rich text from a UITextView into the pasteboard, the pasteboard contains two types:
"public.text",
"Apple Web Archive pasteboard type
Based on that, I created a convenient category on UIPasteboard.
(With heavy use of code from this answer).
It works, but:
The conversion to html format means I will lose custom attributes. Any clean solution will be gladly accepted.
File UIPasteboard+AttributedString.h:
#interface UIPasteboard (AttributedString)
- (void) setAttributedString:(NSAttributedString *)attributedString;
#end
File UIPasteboard+AttributedString.m:
#import <MobileCoreServices/UTCoreTypes.h>
#import "UIPasteboard+AttributedString.h"
#implementation UIPasteboard (AttributedString)
- (void) setAttributedString:(NSAttributedString *)attributedString {
NSString *htmlString = [attributedString htmlString]; // This uses DTCoreText category NSAttributedString+HTML - https://github.com/Cocoanetics/DTCoreText
NSDictionary *resourceDictionary = #{ #"WebResourceData" : [htmlString dataUsingEncoding:NSUTF8StringEncoding],
#"WebResourceFrameName": #"",
#"WebResourceMIMEType" : #"text/html",
#"WebResourceTextEncodingName" : #"UTF-8",
#"WebResourceURL" : #"about:blank" };
NSDictionary *htmlItem = #{ (NSString *)kUTTypeText : [attributedString string],
#"Apple Web Archive pasteboard type" : #{ #"WebMainResource" : resourceDictionary } };
[self setItems:#[ htmlItem ]];
}
#end
Only implemented setter. If you want to write the getter, and/or put it on GitHub, be my guest :)
Instead of involving HTML, the clean solution is to insert NSAttributedString as RTF (plus plaintext fallback) into the paste board:
- (void)setAttributedString:(NSAttributedString *)attributedString {
NSData *rtf = [attributedString dataFromRange:NSMakeRange(0, attributedString.length)
documentAttributes:#{NSDocumentTypeDocumentAttribute: NSRTFTextDocumentType}
error:nil];
self.items = #[#{(id)kUTTypeRTF: [[NSString alloc] initWithData:rtf encoding:NSUTF8StringEncoding],
(id)kUTTypeUTF8PlainText: attributedString.string}];
}
Swift 5
import MobileCoreServices
public extension UIPasteboard {
func set(attributedString: NSAttributedString) {
do {
let rtf = try attributedString.data(from: NSMakeRange(0, attributedString.length), documentAttributes: [NSAttributedString.DocumentAttributeKey.documentType: NSAttributedString.DocumentType.rtf])
items = [[kUTTypeRTF as String: NSString(data: rtf, encoding: String.Encoding.utf8.rawValue)!, kUTTypeUTF8PlainText as String: attributedString.string]]
} catch {
}
}
}
It is quite simple:
#import <MobileCoreServices/UTCoreTypes.h>
NSMutableDictionary *item = [[NSMutableDictionary alloc] init];
NSData *rtf = [attributedString dataFromRange:NSMakeRange(0, attributedString.length)
documentAttributes:#{NSDocumentTypeDocumentAttribute: NSRTFDTextDocumentType}
error:nil];
if (rtf) {
[item setObject:rtf forKey:(id)kUTTypeFlatRTFD];
}
[item setObject:attributedString.string forKey:(id)kUTTypeUTF8PlainText];
UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
pasteboard.items = #[item];
The pasteboard manager in OSX can auto convert between a lot of textual and image types.
For rich text types, you'd usually place RTF into the pasteboard. You can create RTF representation from an attributed string, and vice versa. See the "NSAttributedString Application Kit Additions Reference".
If you have images included as well, then use the RTFd instead of RTF flavor.
I don't know the MIME types for these (I'm used to the Carbon Pasteboard API, not the Cocoa one), but you can convert between UTIs, Pboard and MIME Types using the UTType API.
UTI for RTF is "public.rtf", for RTFd it's "com.apple.flat-rtfd".

Programming multiline mixed uiview types text

How would I elegantly achieve multiline text that includes a combination of links, plain text, and formatted words? Right now, all I can think of is breaking each word into a token and displaying/positioning it differently (UILabel, UIButton, etc), however, this is terribly inefficient. Here is an example of what I am trying to achieve from the comments feature in Instagram.
Notice, formatted link to user name, indented multiline text, and possibility of inline links to hashtags.
Edit Sep 14
So I was able to implement something very similar using FTCoreTextView
It was simple to import/implement. I am using the code as a static library in my ARC project.
The following are the core components:
I use a method to set the style for my cell's FTCoreTextView
(NSArray *)getCoreTextStyle{
NSMutableArray *result = [NSMutableArray array];
FTCoreTextStyle *defaultStyle = [FTCoreTextStyle new];
defaultStyle.name = FTCoreTextTagDefault; //though the default name is already set to FTCoreTextTagDefault
defaultStyle.font = [UIFont fontWithName:#"Helvetica Neue" size:14.f];
defaultStyle.textAlignment = FTCoreTextAlignementLeft;
[result addObject:defaultStyle];
FTCoreTextStyle *linkStyle = [defaultStyle copy];
linkStyle.name = FTCoreTextTagLink;
linkStyle.color = [UIColor blueColor];//[del colorWithHexString:#"3aa98"];
linkStyle.font = [UIFont fontWithName:#"Helvetica Neue" size:14.f];
[result addObject:linkStyle];
return result;
}
Then in your cell you would set the text for the view, assign a delegate, etc. (My FTCoreTextView overrides a plain IBOutlet for a UIView)
[myFTTView setDelegate:self];
[myFTTView addStyles:[self getCoreTextStyle]];
[myFTTView setText:[self parseLinks:text]];
[myFTTView setNeedsDisplay];
Parse links is a method i separately define to search for hashtags and usernames and then set a style when they are found (this code is rough and leaves much to be improved. I am considering switching to CoreTextHyperlink which already has code search for hashtags and #usernames.)
-(NSString *) parseLinks: (NSString *)cap{
NSArray *split = [cap componentsSeparatedByString:#" "];
NSMutableArray *words = [[NSMutableArray alloc] initWithArray:split];
for (NSString *word in split){
if([word hasPrefix:#"#"] || [word hasPrefix:#"#"]){
NSString *reformat = [NSString stringWithFormat:#"%#%#|%#%#", hashlink,word, word, endhashlink];
[words replaceObjectAtIndex:[split indexOfObject:word] withObject:reformat];
}
}
return [words componentsJoinedByString:#" "];
}
Last but not least, the method called after a tag/username is clicked
- (void)coreTextView:(FTCoreTextView *)acoreTextView receivedTouchOnData:(NSDictionary *)data {
NSString *key = [data objectForKey:#"url"];
if([key hasPrefix:#"#"]){
//do something for tag
}
else{
//do something for user.
}
return;
}
Two ways I can think of:
use CoreText with NSAttributedStrings. Difficult to achieve, especially for the links (you'll have to register a listener somewhere, compute on which "link" you "clicked", ...), but provides the best performance.
use a UIWebView and write the HTML on-the-fly. Less performant, but much more flexible.

memory management for CTRunDelegateRef iPhone

I focus on a project to add image support based on OmniGroup's rtf editor.I met the problem when I use CTFramesetterCreateWithAttributedString, and it receives
EXC_BAD_ACCESS
which is caused by a zombie object.
For simplicity, I get the NSAttributedString from a rtfd file which only has a photo. I add the method to parser the image tag on iOS based on the Omni's sample. And the part to create the NSAttributedString is followed by this tutorial from raywenderlich, and looks like:
CTRunDelegateCallbacks callbacks;
callbacks.version = kCTRunDelegateCurrentVersion;
callbacks.getAscent = ascentCallback;
callbacks.getDescent = descentCallback;
callbacks.getWidth = widthCallback;
callbacks.dealloc = deallocCallback;
CTRunDelegateRef delegate = CTRunDelegateCreate(&callbacks, imgAttr);
NSDictionary *imgAttr = [NSDictionary dictionaryWithObjectsAndKeys:imgWidth,kImageWidth,imgHeight,kImageHeight, nil];//recored the width and height
NSDictionary *attrDictionaryDelegate = [NSDictionary dictionaryWithObjectsAndKeys:(id)delegate, (NSString*)kCTRunDelegateAttributeName,nil];
[_attributedString appendString:#" " attributes:attrDictionaryDelegate];
CFRelease(delegate);
here appendString:attributes: is provided by Omni and is a category for NSMutableAttributedString:
- (void)appendString:(NSString *)string attributes:(NSDictionary *)attributes;
{
NSAttributedString *append;
append = [[NSAttributedString alloc] initWithString:string attributes:attributes];
[self appendAttributedString:append];
[append release];
}
In the Parser class I test CTFramesetterCreateWithAttributedString and it creates successfully and no memory problem happens. However, when I call CTFramesetterCreateWithAttributedString in my view class, it receives the EXC_BAD_ACESS problem. I send the CTFramesetterCreateWithAttributedString to my view in the viewDidLoad:
NSArray *arr = [OUIRTFReader parseRTFString2:rtfString];
self.editFrame.attributedText = [arr objectAtIndex:0];
//for OUIRTFReader
+ (NSArray *)parseRTFString2:(NSString *)rtfString
{
OUIRTFReader *parser = [[self alloc] _initWithRTFString:rtfString];
NSMutableArray *temp = [NSMutableArray arrayWithCapacity:1];
NSAttributedString *tempAstr = [[NSAttributedString alloc] initWithAttributedString:parser.attributedString];
[temp addObject:tempAstr];
[tempAstr release];
if (parser.zjImageArray) {
[temp addObject:parser.zjImageArray];
}
[parser release];
}
here editFrame is ivar of OUIEditableFrame provided by OmniFramework.
After that, in the layoutSubview: of OUIEditableFrame, CTFramesetterCreateWithAttributedString failed. Profiling with instrument, it points there is a zombie object in :
NSDictionary *imgAttr = [NSDictionary dictionaryWithObjectsAndKeys:imgWidth,kImageWidth,imgHeight,kImageHeight, nil];
It demonstrates that
An Objective-C message was sent to a deallocated object(zombie) at address
I'm not so familiar with core foundation like as UIKit or others. So I'd like to know what's wrong with the method to create a attributed string for image with CTRunDelegateRef in my code?
Thanks!
CTRunDelegateCreate create a delegate with an arbitrary context pointer (void *). That means the dictionary imgAttr isn't retained by the delegate.
You need to retain the context dictionary when creating the delegate and release it in the dealloc callback.

Resources