Search code examples
iosobjective-cuitextviewnsattributedstring

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

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.


Solution

  • 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;
    }