UITextView: assigning attributedText on iOS 6 leads to unexpected result - ios

I'm trying to implement keywords highlighting using UITextView control.
Here is what's performing in UITextView delegate method:
- (void)textViewDidChange:(UITextView *)textView {
NSAttributedString *attrStr = textView.attributedText;
NSString * string = [attrStr string];
NSRegularExpression* regex = NameRegularExpression();
NSArray * matches = [regex matchesInString:string options:0 range:NSMakeRange(0, [string length])];
NSMutableAttributedString *attrMutableStr = [[NSMutableAttributedString alloc] initWithString:string];
for (NSTextCheckingResult* match in matches ) {
[attrMutableStr addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:match.range];
}
textView.attributedText = attrMutableStr;
textView.contentSize = CGSizeMake(textView.frame.size.width, textView.contentSize.height);
}
User input parses with regexp and all extracted elements are highlighted. It works perfect on iOS 7 but absolutely crazy on iOS 6:
Any suggestions of what am I doing wrong?
By the way, it happens even if just assign exactly the same attributedString back:
- (void)textViewDidChange:(UITextView *)textView {
NSAttributedString *attrStr = textView.attributedText;
textView.attributedText = attrStr;
}

Related

How the find the all the ranges of all strings between quotation marks

I have been working on this problem for a couple of days but can't find a solution to it. I have a very long string that contains a lot of quotes and I want to be able to find all of the strings (the ranges of this strings) so that I can bold them using NSMutableAttributedString
Example
This is a string "with quoted" text and there is "some more" quoted text.
I want to be able to turn this string into the following:
This is a string "with quoted" text and there is "some more" quoted text.
This is what I have so far but it won't do the rest of the string:
- (void)stringBetweenString:(NSString*)start andString:(NSString*)end inString:(NSMutableAttributedString *)text
{
NSRange startRange = [[text string] rangeOfString:start];
if (startRange.location != NSNotFound)
{
NSRange targetRange;
targetRange.location = startRange.location + startRange.length;
targetRange.length = [text length] - targetRange.location;
NSRange endRange = [[text string] rangeOfString:end options:0 range:targetRange];
if (endRange.location != NSNotFound)
{
targetRange.length = endRange.location - targetRange.location;
[text addAttribute:NSFontAttributeName value:[NSFont fontWithName:#"Helvetica-Bold" size:12.0] range:targetRange];
}
}
}
If text is too long this could be little bit slow however it works. Working on Regex solution.
NSString *string = #"Example This is a string \"with quoted1\" text and there is \"some more1\" quoted text. I want to be able to turn this string into the following: This is a string \"with quoted2\" text and there is \"some more2\" quoted text.";
NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:string attributes:nil];
int leftFromLeft = 0;
while ([string rangeOfString:#"\""].location != NSNotFound) {
NSRange quoteLocationFirst = [string
rangeOfString:#"\""
options:0
range:NSMakeRange(leftFromLeft, string.length - leftFromLeft)
];
leftFromLeft = quoteLocationFirst.location + quoteLocationFirst.length;
NSRange quoteLocationSecond = [string
rangeOfString:#"\""
options:0
range:NSMakeRange(leftFromLeft, string.length - leftFromLeft)
];
NSRange quotedTextRange = NSMakeRange(
quoteLocationFirst.location,
quoteLocationSecond.location - quoteLocationFirst.location + 1
);
UIFont *font = [UIFont fontWithName:#"Helvetica-Bold" size:30.0f];
[attString addAttribute:NSFontAttributeName value:font range:quotedTextRange];
NSLog(#"%# \r\n\r\n", [string substringWithRange:quotedTextRange]);
leftFromLeft = quoteLocationSecond.location + quoteLocationSecond.length;
if ([string rangeOfString:#"\"" options:0 range:NSMakeRange(leftFromLeft, string.length - leftFromLeft)].location == NSNotFound) {
string = #"";
}
}
Edit
Regex solution appears to be better/faster.
NSString *string = #"Example This is a string \"with quoted1\" text and there is \"some more1\" quoted text. I want to be able to turn this string into the following: This is a string \"with quoted2\" text and there is \"some more2\" quoted text. Example This is a string \"with quoted3\" text and there is \"some more3\" quoted text. I want to be able to turn this string into the following: This is a string \"with quoted4\" text and there is \"some more4\" quoted text.";
NSMutableAttributedString *attString = [[NSMutableAttributedString alloc] initWithString:string attributes:nil];
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"\"([^\"]*)\"" options:NSRegularExpressionCaseInsensitive error:nil];
NSArray *arrayOfAllMatches = [regex matchesInString:string options:0 range:NSMakeRange(0, string.length)];
for (NSTextCheckingResult *match in arrayOfAllMatches) {
UIFont *font = [UIFont fontWithName:#"Helvetica-Bold" size:30.0f];
[attString addAttribute:NSFontAttributeName value:font range:match.range];
//NSLog(#"%#", [string substringWithRange:match.range]);
}

Display edited NSString in order

I have been working on this for a few days with help from this great community.
I have a NSArray that I need to edit NSStrings within. I have managed to detect a marker in the string and make it bold. However now I am trying to display the strings in the order that they are within the NSArray whilst maintaining the Bold that was added to the specific strings.
I can display the individual Bold String 'string' but I need it to be in order that it is within the array. I know of stringByAppendingString but this would put it at the end.
Any directions would be brilliant.
for (NSString *testWord in legislationArray) {
if ([testWord rangeOfString:#"BOLDME"].location != NSNotFound) {
//Remove Marker
NSString *stripped = [testWord stringByReplacingOccurrencesOfString:#"BOLDME" withString:#""];
//Get string and add bold
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:stripped];
NSRange selectedRange = [stripped rangeOfString:(stripped)];
[string beginEditing];
[string addAttribute:NSFontAttributeName
value:[UIFont fontWithName:#"Helvetica-Bold" size:18.0]
range:selectedRange];
[string endEditing];
//Where to go now with string?
}
}
cell.dynamicLabel.text = [legislationArray componentsJoinedByString:#"\n"];
EDIT
Based on the answers below I got it working however the bold method invokes this error:
componentsJoinedByString return a NSString, when you want a NSAttributedString.
Plus, you're setting your text to a receiver that awaits a NSString (cell.dynamicLabel.text), where what you want should be cell.dynamicLabel.attributedText.
Since there is no equivalent to componentsJoinedByString for a NSAttributedString return, you have to do it the oldway, with a for loop, starting with initializing a NSMutableAttributedString, and adding to it each components (that you may "transform") to it.
Here is a example and related question.
Just use additional array. Change your code to
NSMutableAttributedString *attrString = [[NSMutableAttributedString alloc] init];
for (NSString *testWord in legislationArray) {
if ([testWord rangeOfString:#"BOLDME"].location != NSNotFound) {
//Remove Marker
NSString *stripped = [testWord stringByReplacingOccurrencesOfString:#"BOLDME" withString:#""];
//Get string and add bold
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:stripped];
NSRange selectedRange = [stripped rangeOfString:(stripped)];
[string beginEditing];
[string addAttribute:NSFontAttributeName
value:[UIFont fontWithName:#"Helvetica-Bold" size:18.0]
range:selectedRange];
[string endEditing];
//Where to go now with string?
[attrString appendAttributedString:string];
}
else
{
[attrString appendAttributedString:[[NSAttributedString alloc] initWithString:testWord]];
}
// NEW LINE
[attrString appendAttributedString:[[NSAttributedString alloc] initWithString:#"\n"]];
}
cell.dynamicLabel.attributedText = attrString;
UPDATE:
Your additional issue is not a error - this is a way how XCode shows attributed strings in debug window:

CGRect for selected UITextRange adjustment for multiline text?

I've used this answer in order to create a CGRect for a certain range of text.
In this UITextView I've set it's attributedText (so I've got a bunch of styled text with varying glyph sizes).
This works great for the first line of text that's left aligned, but it has some really strange results when working with NSTextAlignmentJustified or NSTextAlignmentCenter.
It also doesn't calculate properly when the lines wrap around or (sometimes) if there are \n line breaks.
I get stuff like this (this is center aligned):
When instead I expect this:
This one has a \n line break - the first two code bits were highlighted successfully, but the last one more code for you to see was not because the text wrapping isn't factored into the x,y calculations.
Here's my implementation:
- (void)formatMarkdownCodeBlockWithAttributes:(NSDictionary *)attributesDict
withHighlightProperties:(NSDictionary *)highlightProperties
forFontSize:(CGFloat)pointSize
{
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"`.+?`" options:NO error:nil];
NSArray *matchesArray = [regex matchesInString:[self.attributedString string] options:NO range:NSMakeRange(0, self.attributedString.length)];
for (NSTextCheckingResult *match in matchesArray)
{
NSRange range = [match range];
if (range.location != NSNotFound) {
self.textView.attributedText = self.attributedString;
CGRect codeRect = [self frameOfTextRange:range forString:[[self.attributedString string] substringWithRange:range] forFontSize:pointSize];
UIView *highlightView = [[UIView alloc] initWithFrame:codeRect];
highlightView.layer.cornerRadius = 4;
highlightView.layer.borderWidth = 1;
highlightView.backgroundColor = [highlightProperties valueForKey:#"backgroundColor"];
highlightView.layer.borderColor = [[highlightProperties valueForKey:#"borderColor"] CGColor];
[self.contentView insertSubview:highlightView atIndex:0];
[self.attributedString addAttributes:attributesDict range:range];
//strip first and last `
[[self.attributedString mutableString] replaceOccurrencesOfString:#"(^`|`$)" withString:#" " options:NSRegularExpressionSearch range:range];
}
}
}
- (CGRect)frameOfTextRange:(NSRange)range forString:(NSString *)string forFontSize:(CGFloat)pointSize
{
self.textView.selectedRange = range;
UITextRange *textRange = [self.textView selectedTextRange];
CGRect rect = [self.textView firstRectForRange:textRange];
//These three lines are a workaround for getting the correct width of the string since I'm always using the monospaced Menlo font.
rect.size.width = ((pointSize / 1.65) * string.length) - 4;
rect.origin.x+=2;
rect.origin.y+=2;
return rect;
}
Oh, and in case you want it, here's the string I'm playing with:
*This* is **awesome** #mention `code` more \n `code and code` #hashtag [markdown](http://google.com) __and__ #mention2 {#FFFFFF|colored text} This**will also** work but ** will not ** **work** Also, some `more code for you to see`
Note: Please don't suggest I use TTTAttributedLabel or OHAttributedLabel.
I think all your problems are because of incorrect order of instructions.
You have to
Set text aligment
Find required substrings and add specific attributes to them
And only then highlight strings with subviews.
Also you will not need to use "a workaround for getting the correct width of the string since I'm always using the monospaced Menlo font" in such a case.
I have simplified your code a little to make it more understandable.
Result:
- (void)viewDidLoad
{
[super viewDidLoad];
NSDictionary *basicAttributes = #{ NSFontAttributeName : [UIFont boldSystemFontOfSize:18],
NSForegroundColorAttributeName : [UIColor blackColor] };
NSDictionary *attributes = #{ NSFontAttributeName : [UIFont systemFontOfSize:15],
NSForegroundColorAttributeName : [UIColor darkGrayColor]};
_textView.attributedText = [[NSAttributedString alloc] initWithString:
#"*This* is **awesome** #mention `code` more \n `code and code` #hashtag [markdown](http://google.com) __and__ #mention2 {#FFFFFF|colored text} This**will also** work but ** will not ** **work** Also, some `more code for you to see`" attributes:attributes];
_textView.textAlignment = NSTextAlignmentCenter;
[self formatMarkdownCodeBlockWithAttributes:basicAttributes];
}
- (void)formatMarkdownCodeBlockWithAttributes:(NSDictionary *)attributesDict
{
NSMutableString *theString = [_textView.attributedText.string mutableCopy];
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"`.+?`" options:NO error:nil];
NSArray *matchesArray = [regex matchesInString:theString options:NO range:NSMakeRange(0, theString.length)];
NSMutableAttributedString *theAttributedString = [_textView.attributedText mutableCopy];
for (NSTextCheckingResult *match in matchesArray)
{
NSRange range = [match range];
if (range.location != NSNotFound) {
[theAttributedString addAttributes:attributesDict range:range];
}
}
_textView.attributedText = theAttributedString;
for (NSTextCheckingResult *match in matchesArray)
{
NSRange range = [match range];
if (range.location != NSNotFound) {
CGRect codeRect = [self frameOfTextRange:range];
UIView *highlightView = [[UIView alloc] initWithFrame:codeRect];
highlightView.layer.cornerRadius = 4;
highlightView.layer.borderWidth = 1;
highlightView.backgroundColor = [UIColor yellowColor];
highlightView.layer.borderColor = [[UIColor redColor] CGColor];
[_textView insertSubview:highlightView atIndex:0];
}
}
}
- (CGRect)frameOfTextRange:(NSRange)range
{
self.textView.selectedRange = range;
UITextRange *textRange = [self.textView selectedTextRange];
CGRect rect = [self.textView firstRectForRange:textRange];
return rect;
}
I just had to do something similar to this. Assuming you are using iOS 7:
// Build the range that you want for your text
NSRange range = NSMakeRange(location, length);
// Get the substring of the attributed text at that range
NSAttributedString *substring = [textView.attributedText attributedSubstringFromRange:range];
// Find the frame that would enclose the substring of text.
CGRect frame = [substring boundingRectWithSize:maxSize
options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
context:nil];
This should use the NSTextAlignment assigned to the attributed string.
As #Avt answered https://stackoverflow.com/a/22572201/3549781 this question. I'm just answering for the newline problem. This newline problem occurs on iOS 7+ even if you use
[self.textView selectedTextRange] or [self.textView positionFromPosition: offset:]
We just have to ensure the layout of the textView before calling firstRectForRange by
[self.textView.layoutManager ensureLayoutForTextContainer:self.textView.textContainer];
Courtesy : https://stackoverflow.com/a/25983067/3549781
P.S : At first I added this as a comment to the question. As most people don't read comments I added this as an answer.

Change attributes of substrings in a NSAttributedString

This question may be a duplicate of this one. But the answers don't work for me and I want to be more specific.
I have a NSString, but I need a NS(Mutable)AttributedString and some of the words in this string should be given a different color. I tried this:
NSString *text = #"This is the text and i want to replace something";
NSDictionary *attributes = # {NSForegroundColorAttributeName : [UIColor redColor]};
NSMutableAttributedString *subString = [[NSMutableAttributedString alloc] initWithString:#"AND" attributes:attributes];
NSMutableAttributedString *newText = [[NSMutableAttributedString alloc] initWithString:text];
newText = [[newText mutableString] stringByReplacingOccurrencesOfString:#"and" withString:[subString mutableString]];
The "and" should be uppercase an red.
The documentation says that mutableString keeps the attribute mappings. But with my replacing-thing, I have no more attributedString on the right side of the assignment (in the last line of my code-snippet).
How can I get what I want? ;)
#Hyperlord's answer will work, but only if there is one occurence of the word "and" in the input string. Anyway, what I would do is use NSString's stringByReplacingOccurrencesOfString: initially to change every "and" to an "AND", then use a little regex to detect matches in attributed string, and apply NSForegroundColorAttributeName at that range. Here's an example:
NSString *initial = #"This is the text and i want to replace something and stuff and stuff";
NSString *text = [initial stringByReplacingOccurrencesOfString:#"and" withString:#"AND"];
NSMutableAttributedString *mutableAttributedString = [[NSMutableAttributedString alloc] initWithString:text];
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"(AND)" options:kNilOptions error:nil];
NSRange range = NSMakeRange(0,text.length);
[regex enumerateMatchesInString:text options:kNilOptions range:range usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
NSRange subStringRange = [result rangeAtIndex:1];
[mutableAttributedString addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:subStringRange];
}];
And finally, just apply the attributed string to your label.
[myLabel setAttributedText:mutableAttributedString];
I think you should create a NSMutableAttributedString using the existing NSString and then add the style attributes with the appropriate NSRange in order to colorize the parts you want to emphasize for example:
NSString *text = #"This is the text and i want to replace something";
NSMutableAttributedString *mutable = [[NSMutableAttributedString alloc] initWithString:text];
[mutable addAttribute: NSForegroundColorAttributeName value:[UIColor redColor] range:[text rangeOfString:#"and"]];
Be aware: this is just from my head and not tested at all ;-)
Please try this code in Swift 2
var someStr = "This is the text and i want to replace something"
someStr.replaceRange(someStr.rangeOfString("and")!, with: "AND")
let attributeStr = NSMutableAttributedString(string: someStr)
attributeStr.setAttributes([NSForegroundColorAttributeName: UIColor.yellowColor()], range: NSMakeRange(17, 3) )
testLbl.attributedText = attributeStr
Here's another implementation (in Swift) that's helpful if you're doing some more complex manipulations (such as adding/deleting characters) with your attributed string:
let text = "This is the text and i want to replace something"
let mutAttrStr = NSMutableAttributedString(string: text)
let pattern = "\\band\\b"
let regex = NSRegularExpression(pattern: pattern, options: .allZeros, error: nil)
while let result = regex!.firstMatchInString(mutAttrStr.string, options: .allZeros, range:NSMakeRange(0, count(mutAttrStr.string)) {
let substring = NSMutableAttributedString(attributedString: mutAttrStr.attributedSubstringFromRange(result.range))
// manipulate substring attributes here
substring.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range NSMakeRange(0, count(substring.string))
mutAttrStr.replaceCharactersInRange(result.range, withAttributedString: substring)
}
Your final attributed string should be:
let finalAttrStr = mutAttrStr.copy() as! NSAttributedString

Replace UITextViews text with attributed string

I have a UITextView and when the user is entering text into it, I want to format the text on the fly. Something like syntax highlighting...
For that I'd like to use UITextView...
Everything works fine expect one problem: I take the text from the text view and make an NSAttributedString from of it. I make some edits to this attributed string and set it back as the textView.attributedText.
This happens everytime the user types. So I have to remember the selectedTextRange before the edit to the attributedText and set it back afterwards so that the user can continue typing at the place he was typing before. The only problem is that once the text is long enough to require scrolling, the UITextView will now start scrolling to the top if I type slowly.
Here is some sample code:
- (void)formatTextInTextView:(UITextView *)textView
{
NSRange selectedRange = textView.selectedRange;
NSString *text = textView.text;
// This will give me an attributedString with the base text-style
NSMutableAttributedString *attributedString = [self attributedStringFromString:text];
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"#(\\w+)" options:0 error:&error];
NSArray *matches = [regex matchesInString:text
options:0
range:NSMakeRange(0, text.length)];
for (NSTextCheckingResult *match in matches)
{
NSRange matchRange = [match rangeAtIndex:0];
[attributedString addAttribute:NSForegroundColorAttributeName
value:[UIColor redColor]
range:matchRange];
}
textView.attributedText = attributedString;
textView.selectedRange = selectedRange;
}
Is there any solution without using CoreText directly? I like the UITextViews ability to select text and so on....
I am not sure that this is correct solution, but it works.
Just disable scrolling before formatting text and enable it after formatting
- (void)formatTextInTextView:(UITextView *)textView
{
textView.scrollEnabled = NO;
NSRange selectedRange = textView.selectedRange;
NSString *text = textView.text;
// This will give me an attributedString with the base text-style
NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text];
NSError *error = nil;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"#(\\w+)" options:0 error:&error];
NSArray *matches = [regex matchesInString:text
options:0
range:NSMakeRange(0, text.length)];
for (NSTextCheckingResult *match in matches)
{
NSRange matchRange = [match rangeAtIndex:0];
[attributedString addAttribute:NSForegroundColorAttributeName
value:[UIColor redColor]
range:matchRange];
}
textView.attributedText = attributedString;
textView.selectedRange = selectedRange;
textView.scrollEnabled = YES;
}
Used Sergeys's answer myself and ported it to Swift 2:
func formatTextInTextView(textView: UITextView) {
textView.scrollEnabled = false
let selectedRange = textView.selectedRange
let text = textView.text
// This will give me an attributedString with the base text-style
let attributedString = NSMutableAttributedString(string: text)
let regex = try? NSRegularExpression(pattern: "#(\\w+)", options: [])
let matches = regex!.matchesInString(text, options: [], range: NSMakeRange(0, text.characters.count))
for match in matches {
let matchRange = match.rangeAtIndex(0)
attributedString.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: matchRange)
}
textView.attributedText = attributedString
textView.selectedRange = selectedRange
textView.scrollEnabled = true
}
Swift 2.0:
let myDisplayTxt:String = "Test String"
let string: NSMutableAttributedString = NSMutableAttributedString(string: self.myDisplayTxt)
string.addAttribute(NSForegroundColorAttributeName, value: UIColor.redColor(), range: NSMakeRange(0, 5))
string.addAttribute(String(kCTForegroundColorAttributeName), value: UIColor.redColor().CGColor as AnyObject, range: NSMakeRange(0, 5))
self.sampleTextView.attributedText = string
In Swift 4:
func createAttributedText() {
let stringText = "Test String"
let stringCount = stringText.count
let string: NSMutableAttributedString = NSMutableAttributedString(string: stringText)
string.addAttribute(NSForegroundColorAttributeName, value: UIColor.red, range: NSMakeRange(0, stringCount))
self.textView.attributedText = string
}

Resources