13

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):

enter image description here

When instead I expect this:

enter image description here

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.

Community
  • 1
  • 1
brandonscript
  • 68,675
  • 32
  • 163
  • 220
  • I have been using this technique for adding tapGesture on specific words in a textView, and you are right. As soon as a `\n` is inserted in the text, the `firstRectForRange` is all screwed up. I'll work on a solution and get back to you. – n00bProgrammer Mar 15 '14 at 07:44
  • You keep changing the attributed string and consequently the text view's attributed text. So, you keep invalidating the previous results of `-frameOfTextRange:...`. You need to compose the final attributed string and *then* compute the rectangles to highlight. If it helps to "remember" the ranges that you want to highlight, you can apply a custom attribute to them and then enumerate the ranges. – Ken Thomases Mar 15 '14 at 07:45
  • @KenThomases I see where you're going with this, but the problem is that the ranges are character ranges, not CGPoint ranges, and thus there is no special reference. – brandonscript Mar 15 '14 at 20:56
  • My point is that a given character range may change its location (its rect) as you change the attributed text. So, each time through the loop you invalidate the location of any previously-computed highlight views. – Ken Thomases Mar 15 '14 at 21:48
  • @KenThomases alright, that might in fact solve the centered text problem, but how do I accommodate word wrapping? Since I need to generate the NSAttributedString prior to assigning it to the UITextView properties... – brandonscript Mar 15 '14 at 22:04
  • The line wrapping should be accommodated automatically. I suspect the problem is the same. As you changed the attributed string, the line wrapping was changed, too, so the rect for the last code range changed. Also, if you just want to draw a special background, have you considered just using `NSBackgroundColorAttributeName` rather than views? – Ken Thomases Mar 15 '14 at 23:19
  • It's not unfortunately - I can draw a simple NSAttributedString that's long enough to auto-wrap and the rects get drawn off screen, as if it were one line still. As for the `NSBackgroundColorAttributeName`, I could use that (though there are no borders/rounded corners unfortunately) but that doesn't really solve my requirement of drawing the rects for actions and other styling. – brandonscript Mar 15 '14 at 23:25
  • You might need to force layout of the text view after setting its attributed text. `[self.textView.layoutManager ensureLayoutForTextContainer:self.textView.textContainer]` If the text view would draw the text on screen, then it theoretically knows where it is. Ultimately, though, you may wish to use a custom subclass of `NSLayoutManager` to handle custom rendering, again rather than highlight views. – Ken Thomases Mar 16 '14 at 05:04
  • @KenThomases I'll see if that helps - not hopeful but we'll see. If I dive into `NSLayoutManager` territory, I might as well just use TTT. – brandonscript Mar 16 '14 at 05:22
  • @n00bProgrammer Solution for the \n newline problem http://stackoverflow.com/a/25983067/3549781 – iCanCode Jun 09 '15 at 13:02

3 Answers3

4

I think all your problems are because of incorrect order of instructions.

You have to

  1. Set text aligment
  2. Find required substrings and add specific attributes to them
  3. 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: enter image description here

- (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;
}
Avt
  • 16,927
  • 4
  • 52
  • 72
1

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.

lehn0058
  • 19,977
  • 15
  • 69
  • 109
1

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.

Community
  • 1
  • 1
iCanCode
  • 1,001
  • 1
  • 13
  • 24