Highlight just the text in a UILabel

It seemed to me that the line break was the problem.
My idea was to try and know when the UILabel would add a line break and then just remove that character from the range of characters being highlighted.

It appears that you can’t just ask UILabel where the line breaks are going to be but you can check what the size of an NSString will be when you add it to a label.
Using this information you can increment through each character constantly checking the height, and when the height changes you know you have a new line.

I have made an example that takes the Label’s string and separates it into its individual lines that will appear in the UILabel. Once I have each line, I just set the background color on each line instead of the whole string. This eliminates and background colors being set on line breaks.

There are probably better solutions, and this one could probably be optimized for better performance, but it’s a starting point and it appears to work.

Highlight words

- (void)createSomeLabel {
    // Create and position my label
    UILabel *someLabel = [[UILabel alloc] initWithFrame:CGRectMake(0,
                                                                   0,
                                                                   self.view.frame.size.width - 40,
                                                                   self.view.frame.size.height - 300)];
    someLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
    someLabel.textAlignment = NSTextAlignmentCenter;
    someLabel.textColor = [UIColor whiteColor];
    someLabel.lineBreakMode = NSLineBreakByWordWrapping;
    someLabel.numberOfLines = 0;
    [self.view addSubview:someLabel];

    // This string will be different lengths all the time
    NSString *someLongString = @"Here is a really long amount of text that is going to wordwrap/line break and I don't want to highlight the spacing. I want to just highlight the words and a single space before/after the word";

    // Create attributed string
    NSMutableAttributedString *someLongStringAttr=[[NSMutableAttributedString alloc] initWithString:someLongString attributes:nil];


    // The idea here is to figure out where the UILabel would automatically make a line break and get each line of text separately.
    // Temporarily set the label to be that string so that we can guess where the UILabel naturally puts its line breaks.
    [someLabel setText:someLongString];
    // Get an array of each individual line as the UILabel would present it.
    NSArray *allLines = getLinesForLabel(someLabel);
    [someLabel setText:@""];


    // Loop through each line of text and apply the background color to just the text within that range.
    // This way, no whitespace / line breaks will be highlighted.
    __block int startRange = 0;
    [allLines enumerateObjectsUsingBlock:^(NSString *line, NSUInteger idx, BOOL *stop) {

        // The end range should be the length of the line, minus one for the whitespace.
        // If we are on the final line, there are no more line breaks so we use the whole line length.
        NSUInteger endRange = (idx+1 == allLines.count) ?  line.length : line.length-1;

        // Apply background color
        [someLongStringAttr addAttribute:NSBackgroundColorAttributeName
                                   value:[UIColor colorWithWhite:0 alpha:0.25]
                                   range:NSMakeRange(startRange, endRange)];

        // Update the start range to the next line
        startRange += line.length;
    }];



    // Set text of label
    someLabel.attributedText = someLongStringAttr;
}


#pragma mark - Utility Functions

static NSArray *getLinesForLabel(UILabel *label) {

    // Get the text from the label
    NSString *labelText = label.text;

    // Create an array to hold the lines of text
    NSMutableArray *allLines = [NSMutableArray array];

    while (YES) {

        // Get the length of the current line of text
        int length = getLengthOfTextInFrame(label, labelText) + 1;

        // Add this line of text to the array
        [allLines addObject:[labelText substringToIndex:length]];

        // Adjust the label text
        labelText = [labelText substringFromIndex:length];

        // Check for the final line
        if(labelText.length<length) {
            [allLines addObject:labelText];
            break;
        }
    }

    return [NSArray arrayWithArray:allLines];
}

static int getLengthOfTextInFrame(UILabel *label, NSString *text) {

    // Create a block for getting the bounds of the current peice of text.
    CGRect (^boundingRectForLength)(int) = ^CGRect(int length) {
        NSString *cutText = [text substringToIndex:length];
        CGRect textRect = [cutText boundingRectWithSize:CGSizeMake(label.frame.size.width, CGFLOAT_MAX)
                                                options:NSStringDrawingUsesLineFragmentOrigin
                                             attributes:@{NSFontAttributeName : label.font}
                                                context:nil];
        return textRect;
    };

    // Get the frame of the string for one character
    int length = 1;
    int lastSpace = 1;
    CGRect textRect = boundingRectForLength(length);
    CGFloat oneLineHeight = CGRectGetHeight(textRect);

    // Keep adding one character to the string until the height changes, then you know you have a new line
    while (textRect.size.height <= oneLineHeight)
    {
        // If the next character is white space, save the current length.
        // It could be the end of the line.
        // This will not work for character wrap.
        if ([[text substringWithRange:NSMakeRange (length, 1)] isEqualToString:@" "]) {
            lastSpace = length;
        }

        // Increment length and get the new bounds
        textRect = boundingRectForLength(++length);
    }

    return lastSpace;
}

Leave a Comment