6

I just want to create a UITextView like this (not two textviews, the empty area is a uiimage)

enter image description here

Tieme
  • 62,602
  • 20
  • 102
  • 156
ericyue
  • 646
  • 1
  • 8
  • 20

2 Answers2

15

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:

ShapedLabel screen shot

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
rob mayoff
  • 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
  • I don't really understand what you're asking for. – rob mayoff Jul 31 '13 at 19:30
  • 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
  • You need to post a separate question for that. – rob mayoff Jul 31 '13 at 20:05
  • 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
0

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:

Text wraps around image

Full example on github

Tieme
  • 62,602
  • 20
  • 102
  • 156