I just want to create a UITextView like this (not two textviews, the empty area is a uiimage)
-
the empty area is a uiimage :) – ericyue Feb 14 '12 at 02:43
-
Do you want to let the user edit the text? Or do you just need to show the text in a non-rectangular shape? – rob mayoff Feb 14 '12 at 03:13
2 Answers
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

- 375,296
- 67
- 796
- 848
-
Rob, can you please comment on how to add lineBreakMode by truncating tail to the end of the paragraph? thanks. – x89a10 Jul 31 '13 at 19:24
-
-
Basically I like to show the "..." at the end of the paragraph when the text doesn't fit in the drawing area, is this possible? – x89a10 Jul 31 '13 at 19:33
-
-
Rob, I posted a question here: http://stackoverflow.com/questions/17981211/how-to-draw-a-non-rectangle-uilabel-with-paragraph-truncation-at-the-end , would be great if you can take a look at it. Thanks. – x89a10 Jul 31 '13 at 21:20
-
You should also set accessibility traits so that VoiceOver users can read this. `isAccessibilityElement = true`, `accessibilityTraits = UIAccessibilityTraitStaticText`, and `accessibilityLabel = string`. – user664939 Jan 26 '17 at 22:52
As Dannie P suggested here, use textView.textContainer.exclusionPaths
Example in Swift:
class WrappingTextVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let textView = UITextView()
textView.translatesAutoresizingMaskIntoConstraints = false
textView.text = "ropcap example. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris aliquam vulputate ex. Fusce interdum ultricies justo in tempus. Sed ornare justo in purus dignissim, et rutrum diam pulvinar. Quisque tristique eros ligula, at dictum odio tempor sed. Fusce non nisi sapien. Donec libero orci, finibus ac libero ac, tristique pretium ex. Aenean eu lorem ut nulla elementum imperdiet. Ut posuere, nulla ut tincidunt viverra, diam massa tincidunt arcu, in lobortis erat ex sed quam. Mauris lobortis libero magna, suscipit luctus lacus imperdiet eu. Ut non dignissim lacus. Vivamus eget odio massa. Aenean pretium eget erat sed ornare. In quis tortor urna. Quisque euismod, augue vel pretium suscipit, magna diam consequat urna, id aliquet est ligula id eros. Duis eget tristique orci, quis porta turpis. Donec commodo ullamcorper purus. Suspendisse et hendrerit mi. Nulla pellentesque semper nibh vitae vulputate. Pellentesque quis volutpat velit, ut bibendum magna. Morbi sagittis, erat rutrum Suspendisse potenti. Nulla facilisi. Praesent libero est, tincidunt sit amet tempus id, blandit sit amet mi. Morbi sed odio nunc. Mauris lobortis elementum orci, at consectetur nisl egestas a. Pellentesque vel lectus maximus, semper lorem eget, accumsan mi. Etiam semper tellus ac leo porta lobortis."
textView.backgroundColor = .lightGray
textView.textColor = .black
view.addSubview(textView)
textView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 20).isActive = true
textView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20).isActive = true
textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20).isActive = true
textView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -40).isActive = true
let imageView = UIImageView(image: UIImage(named: "so-icon"))
imageView.backgroundColor = .lightText
imageView.frame = CGRect(x: 0, y: 0, width: 0, height: 0)
imageView.sizeToFit()
textView.addSubview(imageView)
textView.textContainer.exclusionPaths = [UIBezierPath(rect: imageView.frame)]
}
}
Result:

- 62,602
- 20
- 102
- 156