There's no built-in UIView
subclass that does this (except UIWebView
if you write the proper HTML and CSS), but it's quite easy to do using Core Text. I've put my test project in my ShapedLabel github repository, and here's what it looks like:
The project has a UIView
subclass called ShapedLabel
. Here's how it works.
Create a UIView
subclass called ShapedLabel
. Give it these properties:
@property (nonatomic, copy) NSString *text;
@property (nonatomic) UITextAlignment textAlignment;
@property (nonatomic, copy) NSString *fontName;
@property (nonatomic) CGFloat fontSize;
@property (nonatomic, strong) UIColor *textColor;
@property (nonatomic, strong) UIColor *shapeColor;
@property (nonatomic, copy) UIBezierPath *path;
You'll want to override each property setter method to send setNeedsDisplay
, like this for example:
- (void)setFontName:(NSString *)fontName {
_fontName = [fontName copy];
[self setNeedsDisplay];
}
I'm relying on ARC to worry about releasing the old value of _fontName
. If you're not using ARC... start. It's so much easier and it's supported since iOS 4.0.
Anyway, then you'll need to implement drawRect:
, where the real work gets done. First, we'll fill in the shape with the shapeColor
if it's set:
- (void)drawRect:(CGRect)rect
{
if (!_path)
return;
if (_shapeColor) {
[_shapeColor setFill];
[_path fill];
}
We check to make sure we have all the other parameters we need:
if (!_text || !_textColor || !_fontName || _fontSize <= 0)
return;
Next we handle the textAligment
property:
CTTextAlignment textAlignment = NO ? 0
: _textAlignment == UITextAlignmentCenter ? kCTCenterTextAlignment
: _textAlignment == UITextAlignmentRight ? kCTRightTextAlignment
: kCTLeftTextAlignment;
CTParagraphStyleSetting paragraphStyleSettings[] = {
{
.spec = kCTParagraphStyleSpecifierAlignment,
.valueSize = sizeof textAlignment,
.value = &textAlignment
}
};
CTParagraphStyleRef style = CTParagraphStyleCreate(paragraphStyleSettings, sizeof paragraphStyleSettings / sizeof *paragraphStyleSettings);
We create the CTFont
next. Note that this is different than a CGFont
or a UIFont
. You can convert a CGFont
to a CTFont
using CTFontCreateWithGraphicsFont
, but you cannot easily convert a UIFont
to a CTFont
. Anyway we just create the CTFont
directly:
CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)_fontName, _fontSize, NULL);
We create the attributes dictionary that defines all of the style attributes we want to see:
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
(__bridge id)font, kCTFontAttributeName,
_textColor.CGColor, kCTForegroundColorAttributeName,
style, kCTParagraphStyleAttributeName,
nil];
CFRelease(font);
CFRelease(style);
Once we have the attributes dictionary, we can create the attributed string that attaches the attributes dictionary to the text string. This is what Core Text uses:
CFAttributedStringRef trib = CFAttributedStringCreate(NULL, (__bridge CFStringRef)_text, (__bridge CFDictionaryRef)attributes);
We create a Core Text framesetter that will lay out the text from the attributed string:
CTFramesetterRef setter = CTFramesetterCreateWithAttributedString(trib);
CFRelease(trib);
Core Text assumes that the graphics context will have the “standard” Core Graphics coordinate system with the origin at the lower left. But UIKit changes the context to put the origin at the upper left. We'll assume that the path was created with that in mind. So we need a transform that flips the coordinate system vertically:
// Core Text lays out text using the default Core Graphics coordinate system, with the origin at the lower left. We need to compensate for that, both when laying out the text and when drawing it.
CGAffineTransform textMatrix = CGAffineTransformIdentity;
textMatrix = CGAffineTransformTranslate(textMatrix, 0, self.bounds.size.height);
textMatrix = CGAffineTransformScale(textMatrix, 1, -1);
We can then create a flipped copy of the path:
CGPathRef flippedPath = CGPathCreateCopyByTransformingPath(_path.CGPath, &textMatrix);
At last we can ask the framesetter to lay out a frame of text. This is what actually fits the text inside the shape defined by the path
property:
CTFrameRef frame = CTFramesetterCreateFrame(setter, CFRangeMake(0, 0), flippedPath, NULL);
CFRelease(flippedPath);
CFRelease(setter);
Finally we draw the text. We need to again
CGContextRef gc = UIGraphicsGetCurrentContext();
CGContextSaveGState(gc); {
CGContextConcatCTM(gc, textMatrix);
CTFrameDraw(frame, gc);
} CGContextRestoreGState(gc);
CFRelease(frame);
}
That's pretty much it. You can now put a nice shaped label on the screen.
For posterity (in case I delete the test project), here's the complete source for the ShapedLabel
class.
ShapedLabel.h
#import <UIKit/UIKit.h>
@interface ShapedLabel : UIView
@property (nonatomic, copy) NSString *text;
@property (nonatomic) UITextAlignment textAlignment;
@property (nonatomic, copy) NSString *fontName;
@property (nonatomic) CGFloat fontSize;
@property (nonatomic, strong) UIColor *textColor;
@property (nonatomic, strong) UIColor *shapeColor;
@property (nonatomic, copy) UIBezierPath *path;
@end
ShapedLabel.m
#import "ShapedLabel.h"
#import <CoreText/CoreText.h>
@implementation ShapedLabel
@synthesize fontName = _fontName;
@synthesize fontSize = _fontSize;
@synthesize path = _path;
@synthesize text = _text;
@synthesize textColor = _textColor;
@synthesize shapeColor = _shapeColor;
@synthesize textAlignment = _textAlignment;
- (void)commonInit {
_text = @"";
_fontSize = UIFont.systemFontSize;
// There is no API for just getting the system font name, grr...
UIFont *uiFont = [UIFont systemFontOfSize:_fontSize];
_fontName = [uiFont.fontName copy];
}
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self commonInit];
}
return self;
}
- (id)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
[self commonInit];
}
return self;
}
- (void)setFontName:(NSString *)fontName {
_fontName = [fontName copy];
[self setNeedsDisplay];
}
- (void)setFontSize:(CGFloat)fontSize {
_fontSize = fontSize;
[self setNeedsDisplay];
}
- (void)setPath:(UIBezierPath *)path {
_path = [path copy];
[self setNeedsDisplay];
}
- (void)setText:(NSString *)text {
_text = [text copy];
[self setNeedsDisplay];
}
- (void)setTextColor:(UIColor *)textColor {
_textColor = textColor;
[self setNeedsDisplay];
}
- (void)setTextAlignment:(UITextAlignment)textAlignment {
_textAlignment = textAlignment;
[self setNeedsDisplay];
}
- (void)setShapeColor:(UIColor *)shapeColor {
_shapeColor = shapeColor;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
if (!_path)
return;
if (_shapeColor) {
[_shapeColor setFill];
[_path fill];
}
if (!_text || !_textColor || !_fontName || _fontSize <= 0)
return;
CTTextAlignment textAlignment = NO ? 0
: _textAlignment == UITextAlignmentCenter ? kCTCenterTextAlignment
: _textAlignment == UITextAlignmentRight ? kCTRightTextAlignment
: kCTLeftTextAlignment;
CTParagraphStyleSetting paragraphStyleSettings[] = {
{
.spec = kCTParagraphStyleSpecifierAlignment,
.valueSize = sizeof textAlignment,
.value = &textAlignment
}
};
CTParagraphStyleRef style = CTParagraphStyleCreate(paragraphStyleSettings, sizeof paragraphStyleSettings / sizeof *paragraphStyleSettings);
CTFontRef font = CTFontCreateWithName((__bridge CFStringRef)_fontName, _fontSize, NULL);
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
(__bridge id)font, kCTFontAttributeName,
_textColor.CGColor, kCTForegroundColorAttributeName,
style, kCTParagraphStyleAttributeName,
nil];
CFRelease(font);
CFRelease(style);
CFAttributedStringRef trib = CFAttributedStringCreate(NULL, (__bridge CFStringRef)_text, (__bridge CFDictionaryRef)attributes);
CTFramesetterRef setter = CTFramesetterCreateWithAttributedString(trib);
CFRelease(trib);
// Core Text lays out text using the default Core Graphics coordinate system, with the origin at the lower left. We need to compensate for that, both when laying out the text and when drawing it.
CGAffineTransform textMatrix = CGAffineTransformIdentity;
textMatrix = CGAffineTransformTranslate(textMatrix, 0, self.bounds.size.height);
textMatrix = CGAffineTransformScale(textMatrix, 1, -1);
CGPathRef flippedPath = CGPathCreateCopyByTransformingPath(_path.CGPath, &textMatrix);
CTFrameRef frame = CTFramesetterCreateFrame(setter, CFRangeMake(0, 0), flippedPath, NULL);
CFRelease(flippedPath);
CFRelease(setter);
CGContextRef gc = UIGraphicsGetCurrentContext();
CGContextSaveGState(gc); {
CGContextConcatCTM(gc, textMatrix);
CTFrameDraw(frame, gc);
} CGContextRestoreGState(gc);
CFRelease(frame);
}
@end