I see lots of posts asking about the existence of a clickable, rich text UILabel and the answer is often the same…. it’s not possible so use a UIWebView…. NOOOOOOO! The idea of contemplating using such a heavyweight control for a purpose for which is was not designed is a tad crazy. So I decided to write a component to solve the issue and make it flexible to boot. Let’s dive in!

 

The Basics

This component consists of two parts, the label and the style. The label is a relatively straightforward UILabel descendent and the style object is a simple class holding the relative properties for a certain style, of which we can have any number. So let’s start with the style.

The style contains the following properties:

UIFont *font;
UIColor *color;
id target;
SEL action;

 

It contains the font to use, the colour to use and an optional target and action. If these optional parameters are specified then you will be able to tap the text and call the relevant action, otherwise the text will merely be styled.

The second part is the label. This is a bit more complicated, but luckily the full source code is in the link above. The label maintains a list of style objects for a given prefix, so we can style ‘#’ elements, ‘@” elements, ‘http://’ elements etc. It contains a method to associate a style to a prefix as follows….

- (void)addStyle:(LORichTextLabelStyle *)aStyle forPrefix:(NSString *)aPrefix {
if((aPrefix == nil) || (aPrefix.length == 0)) {
[NSException raise:NSInternalInconsistencyException format:@"Prefix must be specified in %@", NSStringFromSelector(_cmd)];
}

[highlightStyles setObject:aStyle forKey:aPrefix];
}

 

Nice and simple. There are overridden methods for the setFont, setColour and setText which set the base font and colour and notify the OS that the label needs to be redrawn. The setText method also splits the text into elements which are used in the layout code later on.

- (void)setFont:(UIFont *)value {
if([font isEqual:value]) {
return;
}

[font release];
font = value;
[font retain];

[self setNeedsDisplay];
}

- (void)setTextColor:(UIColor *)value {
if([textColor isEqual:value]) {
return;
}

[textColor release];
textColor = value;
[textColor retain];

[self setNeedsLayout];
}

- (void)setText:(NSString *)value {
[elements release];
elements = [value componentsSeparatedByString:@" "];
[elements retain];

[self setNeedsLayout];
}

 

Amazing Layout

The only other method in the label is the layout method, and this is where the magic happens. First we remove all subview as we will be creating them all again in a second. We then iterate our elements and process each one, maintaining a layout position as we go. We look for a style matching the prefix of the current element, or the base style if none is specified. We size the element and render it in position with the specified style. If the element has a target we use a UIButton, if not then a UILabel. The full code is as follows…

- (void)layoutSubviews {
[self removeSubviews];

NSUInteger maxHeight = 999999;
CGPoint position = CGPointZero;
CGSize measureSize = CGSizeMake(self.size.width, maxHeight);

for(NSString *element in elements) {
LORichTextLabelStyle *style = nil;

// Find suitable style
for(NSString *prefix in [highlightStyles allKeys]) {
if([element hasPrefix:prefix]) {
style = [highlightStyles objectForKey:prefix];
break;
}
}

UIFont *styleFont = style.font == nil ? font : style.font;
UIColor *styleColor = style.color == nil ? textColor : style.color;

// Get size of content (check current line before starting new one)
CGSize remainingSize = CGSizeMake(measureSize.width - position.x, maxHeight);
CGSize singleLineSize = CGSizeMake(remainingSize.width, 0.0);

CGSize controlSize = [element sizeWithFont:styleFont constrainedToSize:singleLineSize lineBreakMode:UILineBreakModeTailTruncation];
CGSize elementSize = [element sizeWithFont:styleFont constrainedToSize:remainingSize];

if(elementSize.height > controlSize.height) {
position.y += controlSize.height;
position.x = 0.0;
}

elementSize = [element sizeWithFont:styleFont constrainedToSize:measureSize];
CGRect elementFrame = CGRectMake(position.x, position.y, elementSize.width, elementSize.height);

// Add button or label depending on whether we have a target
if(style.target != nil) {
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
Button Text;
Button Text;
Button Text;
Button Text;
Button Text;
[self addSubview:button];
} else {
UILabel *label = [[UILabel alloc] initWithFrame:elementFrame];
[label setBackgroundColor:[UIColor clearColor]];
[label setNumberOfLines:maxHeight];
[label setFont:styleFont];
[label setTextColor:styleColor];
[label setText:element];
[self addSubview:label];
}

CGSize spaceSize = [@" " sizeWithFont:styleFont];
position.x += elementSize.width + spaceSize.width;

if([element isEqual:[elements lastObject]]) {
position.y += controlSize.height;
}
}

[self setSize:CGSizeMake(self.size.width, position.y)];
}

 

In Use

To use the label we need to create some style, add them to the label and set the text accordingly. We can create any number of styles and associate them with specific prefixes. Let’s look at how we would add styles for a hashtag…

// Create a hashtag style
LORichTextLabelStyle *hashStyle = [LORichTextLabelStyle styleWithFont:[UIFont fontWithName:@"Helvetica-Bold" size:14.0] color:[UIColor magentaColor]];
[hashStyle addTarget:self action:@selector(hashSelected:)];

// Create the label with the default font and colour for the specified width
LORichTextLabel *label = [[LORichTextLabel alloc] initWithWidth:300.0];
[label setFont:[UIFont fontWithName:@"Helvetica" size:14.0]];
[label setTextColor:[UIColor blackColor]];
[label setBackgroundColor:[UIColor clearColor]];

// Add the style to the label for the '#' prefix
[label addStyle:hashStyle forPrefix:@"#"];

// Set the text of the label
[label setText:@"This is an example of a rich text UILabel class highlighting #hashtags pretty easily!"];

 

This renders the text as per the style and fires the hashSelected: selector when the tag is clicked. But what does that return? Well the tag is a UIButton so it follows the standard signature, and the title of the button is the tag itself!

- (void)hashSelected:(id)sender
NSString *tag = ((UIButton *)sender).titleLabel.text;
}

 

There you have it. I will leave it to you to look at how to improve this to use the minimum number of UILabel components as it gets a bit more complex but this should be enough to get you started. UIWebviews?! Pah.

You might also be interested in...