Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Highlight just the text in a UILabel

I'm attempting to set the background color/highlight just the text within a UILabel. The issue is that the line breaks and spaces added to the UILabel to keep the text centered are also being highlighted.

enter image description here

Notice the spacing before the last line in the UILabel is highlighted. Also, the beginning and end of any new lines are also highlighted.

I'm creating the example above with the following code:

-(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];

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

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

The output I'd like to achieve is to highlight only the text and the spaces between the words if there is only one space. The length of the text and the size of the UILabel will constantly be different so hard coding a solution is not an option unfortunately.

like image 682
Daniel Storm Avatar asked Jul 08 '15 13:07

Daniel Storm


3 Answers

I've faced same issue and found out easier solution without huge performance costs. You can simply add TTTAttributedLabel to your project.

My demo project for the question:

#import "TTTAttributedLabel.h"

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    UILabel *label1 = [UILabel new];
    label1.textAlignment = NSTextAlignmentCenter;
    label1.numberOfLines = 0;
    label1.frame = CGRectMake(20, 0, CGRectGetWidth(self.view.frame) - 40, CGRectGetHeight(self.view.frame) / 2.0);
    [self.view addSubview:label1];

    TTTAttributedLabel *label2 = [TTTAttributedLabel new];
    label2.textAlignment = NSTextAlignmentCenter;
    label2.numberOfLines = 0;
    label2.frame = CGRectMake(20, CGRectGetHeight(self.view.frame) / 2.0, CGRectGetWidth(self.view.frame) - 40, CGRectGetHeight(self.view.frame) / 2.0);
    [self.view addSubview:label2];

    NSDictionary *attributes = @{NSBackgroundColorAttributeName:[UIColor blackColor], NSForegroundColorAttributeName:[UIColor whiteColor], NSFontAttributeName:[UIFont systemFontOfSize:32 weight:UIFontWeightBold]};
    NSAttributedString *string = [[NSAttributedString alloc] initWithString:@"Some very long string which can contain newlines and some other stuff" attributes:attributes];
    label1.attributedText = string;
    label2.text = string;
}

@end

enter image description here

like image 25
Timur Bernikovich Avatar answered Nov 12 '22 01:11

Timur Bernikovich


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;
}
like image 193
Sam Avatar answered Nov 12 '22 01:11

Sam


Starting at iOS 10.3 the same code in question now produces the desired result. Not sure if this is a bug or a new feature.

-(void)createSomeLabel {
    // Create and position my label
    UILabel *someLabel = [[UILabel alloc] initWithFrame:CGRectMake(0,
                                                                   0,
                                                                   self.view.frame.size.width - 40.0,
                                                                   self.view.frame.size.height - 300.0)];
    someLabel.center = CGPointMake(self.view.frame.size.width / 2.0, self.view.frame.size.height / 2.0);
    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];

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

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

enter image description here

like image 1
Daniel Storm Avatar answered Nov 12 '22 00:11

Daniel Storm