Layout text on the image with Core Text - ios

guys.
I have do rich text eidtor some work with Core Text.I want to keep fixed line height and it truely can be done with the following code:
NSMutableAttributedString *_attributedText = [[NSMutableAttributedString alloc]initWithAttributedString:_attributedString] ;
CGFloat lineSpace=20;
CTParagraphStyleSetting lineSpaceStyle;
lineSpaceStyle.spec=kCTParagraphStyleSpecifierMaximumLineHeight;
lineSpaceStyle.valueSize=sizeof(lineSpace);
lineSpaceStyle.value=&lineSpace;
//CTLineBreakMode lineBreakMode = kCTLineBreakByCharWrapping;
CTParagraphStyleSetting settings[]={
lineSpaceStyle,
// {.spec = kCTParagraphStyleSpecifierLineBreakMode, .valueSize = sizeof(CTLineBreakMode), .value = (const void*)&lineBreakMode},
};
CTParagraphStyleRef paragraphStyle = CTParagraphStyleCreate(settings, sizeof(settings));
[_attributedText addAttribute:(id)kCTParagraphStyleAttributeName value:(id)paragraphStyle range:NSMakeRange(0, _attributedText.mutableString.length)];
_attributedString = [_attributedText copy];
[_attributedText release];
_attributedText = nil;
CFRelease(paragraphStyle);
Then I want to insert an image into it.And I want type some text on the image with following code:
CTRunDelegateCallbacks callbacks = {
.version = kCTRunDelegateVersion1,
.dealloc = AttachmentRunDelegateDealloc,
.getAscent = AttachmentRunDelegateGetAscent,
.getDescent = AttachmentRunDelegateGetDescent,
.getWidth = AttachmentRunDelegateGetWidth
};
CTRunDelegateRef Rundelegate = CTRunDelegateCreate(&callbacks, [image retain]); //3
NSMutableDictionary *attrDictionaryDelegate = [NSMutableDictionary dictionaryWithDictionary:self.defaultAttributes];
[attrDictionaryDelegate setObject:image
forKey:EGOTextAttachmentAttributeName];
[attrDictionaryDelegate setObject:(id)Rundelegate
forKey:(NSString*)kCTRunDelegateAttributeName];
[attrDictionaryDelegate setObject:fulltext
forKey:EGOTextAttachmentOriginStringKey];
NSAttributedString *newString = [[NSAttributedString alloc] initWithString:EGOTextAttachmentPlaceholderString
attributes:attrDictionaryDelegate];
[attrString replaceCharactersInRange:[result resultByAdjustingRangesWithOffset:attrString.length-astring.length].range
withAttributedString:newString];
In another words,is there some ways to let the text layout on the image with Core Text ?????
Anyone????

Very Easy
keep fixed line height with CoreText,
let the text layout on the image by image.draw(in: frame) ,
maybe frame calculated with CoreText .
here is an example, put a background image below the first word in the line
// here is set up
guard let ctx = UIGraphicsGetCurrentContext(), let f = frameRef else{
return
}
let xHigh = bounds.size.height
ctx.textMatrix = CGAffineTransform.identity
ctx.translateBy(x: 0, y: xHigh)
ctx.scaleBy(x: 1.0, y: -1.0)
guard let lines = CTFrameGetLines(f) as? [CTLine] else{
return
}
// here is an image
let bgGrip = UIImage(named: "grip")
if let pieces = CTLineGetGlyphRuns(line) as? [CTRun]{
let pieceCnt = pieces.count
for i in 0..<pieceCnt{
var p = lineOrigin
if i == 0{
var frame = imageFrame
frame.origin.y = lineOrigin.y + lineAscent - imageHeight + TextContentConst.offsetP.y
bgGrip?.draw(in: frame)
p.x += offsetX
}
ctx.textPosition = p
CTRunDraw(pieces[i], ctx, CFRange(location: 0, length: 0))
}
}

Related

iOS tab bar background image propgrammatically

I want to make a separator between items in my tabor programmatically. Then I wrote this code but it display nothing.
float separator_width = 3f;
UIColor separator_color = UIColor.Red;
float itemWidth = (float)Math.Floor(this.TabBar.Frame.Width / this.TabBar.Items.Length);
UIView bgView = new UIView();
bgView.Frame = new CGRect(0, 0, this.TabBar.Frame.Width, this.TabBar.Frame.Height);
for (int i = 0; i < this.TabBar.Items.Length; i++)
{
UIView separator = new UIView();
separator.Frame = new CGRect(itemWidth * (i + 1) - separator_width / 2, 0, separator_width, this.TabBar.Frame.Height);
bgView.Add(separator);
}
UIGraphics.BeginImageContext(new CGSize(bgView.Frame.Width, bgView.Frame.Height));
bgView.Layer.RenderInContext(UIGraphics.GetCurrentContext());
UIImage tabBarBackground = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
UITabBar.Appearance.BackgroundImage = tabBarBackground;
I tried to write it and display it in another view but still nothing. What can I do ?
Thanks
I found another solution. For these who search for that :
Check out below :
var image = new UIImage();
var path = new UIBezierPath();
var lineHeight = 42.5f;
float itemWidth = (float)Math.Floor(this.TabBar.Frame.Width / this.TabBar.Items.Length);
UIGraphics.BeginImageContextWithOptions(new CGSize(this.TabBar.Frame.Width, this.TabBar.Frame.Height), false, 0);
image.Draw(new CGPoint(0,0));
UIColor.Clear.FromHex(0xF2F2F2).SetStroke();
path.LineWidth = 1;
for (int i = 1; i < this.TabBar.Items.Length; i++)
{
path.MoveTo(new CGPoint(itemWidth * i - path.LineWidth / 2, (this.TabBar.Frame.Height - lineHeight) / 2));
path.AddLineTo(new CGPoint(itemWidth * i - path.LineWidth / 2, lineHeight + (this.TabBar.Frame.Height - lineHeight) / 2));
}
path.Stroke();
image = UIGraphics.GetImageFromCurrentImageContext();
UIGraphics.EndImageContext();
UITabBar.Appearance.BackgroundImage = image;

Drawing UILabel according to the string length

I am drawing label using drawRect and the code looks like something below.
if (productName && productName.length > 0) {
UILabel *productNameLabel = [[UILabel alloc]init];
productNameLabel.numberOfLines = 2;
productNameLabel.attributedText = [self shadowedTextWithString:productName fontName:#"ProximaNovaA-Light" fontSize:productNameLabelFontSize isOfferType:NO];
[productNameLabel sizeToFit];
//drawing the UILabel
[productNameLabel drawTextInRect:CGRectMake(25, labelYPosition, productNameLabel.frame.size.width, productNameLabel.frame.size.height)];
CGContextTranslateCTM(UIGraphicsGetCurrentContext(), 25, labelYPosition);
[productNameLabel.layer renderInContext:UIGraphicsGetCurrentContext()];
CGContextTranslateCTM(UIGraphicsGetCurrentContext(), -25, -labelYPosition);
labelYPosition += productNameLabel.frame.origin.y + productNameLabel.frame.size.height+20;
}
However, the productNameLabel.numberOfLiness = 2 doesn't seem to work at all... If the string has length that exceeds the width of the screen, the text is truncated and the UILabel stays one liner.
Anyone knows how do i do it, so that if the length of the string exceeds the width of screen, the exceeded words will go to the second line?
Thanks!
updated code, still doesn't work !
if (productName && productName.length > 0) {
UILabel *productNameLabel = [[UILabel alloc]init];
productNameLabel.lineBreakMode = YES;
productNameLabel.numberOfLines = 0;
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
style.lineBreakMode = NSLineBreakByTruncatingTail;
NSMutableAttributedString *productNameAttributedString = [self shadowedTextWithString:productName fontName:#"ProximaNovaA-Light" fontSize:productNameLabelFontSize isOfferType:NO];
[productNameAttributedString addAttribute:NSParagraphStyleAttributeName
value:style
range:NSMakeRange(0, productNameAttributedString.length)];
productNameLabel.attributedText = productNameAttributedString;
CGSize constrainedSize = CGSizeMake(paramImageView.image.size.width -50 , 9999);
CGRect requiredHeight = [productNameLabel.attributedText boundingRectWithSize:constrainedSize options:NSStringDrawingUsesLineFragmentOrigin context:nil];
if (requiredHeight.size.width > productNameLabel.frame.size.width) {
requiredHeight = CGRectMake(25,labelYPosition, productNameLabel.frame.size.width, requiredHeight.size.height);
}
CGRect newFrame = productNameLabel.frame;
newFrame.size.height = requiredHeight.size.height;
productNameLabel.frame = newFrame;
productNameLabel.backgroundColor = [UIColor redColor];
[productNameLabel drawTextInRect:CGRectMake(25, labelYPosition, paramImageView.image.size.width-50, requiredHeight.size.height)];
//[productNameLabel drawTextInRect:CGRectMake(25, labelYPosition, 30, productNameLabel.frame.size.height)];
CGContextTranslateCTM(UIGraphicsGetCurrentContext(), 25, labelYPosition);
[productNameLabel.layer renderInContext:UIGraphicsGetCurrentContext()];
CGContextTranslateCTM(UIGraphicsGetCurrentContext(), -25, -labelYPosition);
labelYPosition += productNameLabel.frame.origin.y + productNameLabel.frame.size.height+20;
}
Objective-c
CGSize sizeToFit = [title sizeWithFont:productNameLabel.font constrainedToSize:productNameLabel.frame.size lineBreakMode:productNameLabel.lineBreakMode];
Swift 2.2
var sizeToFit = title.sizeWithFont(productNameLabel.font, constrainedToSize: productNameLabel.frame.size, lineBreakMode: productNameLabel.lineBreakMode)
Swift3.0
var sizeToFit: CGSize = title.size(with: productNameLabel.font, constrainedTo: productNameLabel.frame.size, lineBreakMode: productNameLabel.lineBreakMode)
CGSize constrainedSize = CGSizeMake(self.resizableLable.frame.size.width , 9999);
NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
[UIFont fontWithName:#"HelveticaNeue" size:11.0], NSFontAttributeName,
nil];
NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:#"textToShow" attributes:attributesDictionary];
CGRect requiredHeight = [string boundingRectWithSize:constrainedSize options:NSStringDrawingUsesLineFragmentOrigin context:nil];
if (requiredHeight.size.width > self.resizableLable.frame.size.width) {
requiredHeight = CGRectMake(0,0, self.resizableLable.frame.size.width, requiredHeight.size.height);
}
CGRect newFrame = self.resizableLable.frame;
newFrame.size.height = requiredHeight.size.height;
self.resizableLable.frame = newFrame;
Implement this method to find width of string pass font as an argument you want to give to string and this method will return width of string.
func widthOfString(usingFont font: UIFont) -> CGFloat {
let fontAttributes = [NSFontAttributeName: font]
let size = self.size(attributes: fontAttributes)
return size.width
}
Set this width as a width of the UILabel
Swift-3:
Returns the height of label with padding depending on the text.
func heightForView(text: String, font: UIFont, width: CGFloat) -> CGFloat {
let label = UILabel(frame: CGRect.init(x: 0, y: 0, width: width, height: CGFloat.greatestFiniteMagnitude))
label.numberOfLines = 0
label.lineBreakMode = NSLineBreakMode.byWordWrapping
label.font = font
label.text = text
label.sizeToFit()
return label.frame.height + labelHeightPadding ///extra padding; if needed be.
}

Bounding Rectangle using Core Text

Please correct me if I'm wrong.
I tried to work out the exact bounding rectangle of a character using Core Text. But the height I received was always bigger than the actual height of the drawn character on the screen. In this case, the actual height is around 20 but the function just give me 46 no matter what.
Could anyone shed some light on this?
Thanks.
Here is the code
- (void)viewDidLoad{
[super viewDidLoad];
NSString *testString = #"A";
NSAttributedString *textString = [[NSAttributedString alloc] initWithString:testString attributes:#{
NSFontAttributeName: [UIFont fontWithName:#"Helvetica" size:40]
}];
NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:textString];
NSLayoutManager *textLayout = [[NSLayoutManager alloc] init];
// Add layout manager to text storage object
[textStorage addLayoutManager:textLayout];
// Create a text container
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:self.view.bounds.size];
// Add text container to text layout manager
[textLayout addTextContainer:textContainer];
NSRange range = NSMakeRange (0, testString.length);
CGRect boundingBox = [textLayout boundingRectForGlyphRange:range inTextContainer:textContainer];
//BoundingBox:{{5, 0}, {26.679688, 46}}
// Instantiate UITextView object using the text container
UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(20,20,self.view.bounds.size.width-20,self.view.bounds.size.height-20)
textContainer:textContainer];
// Add text view to the main view of the view controler
[self.view addSubview:textView];
}
I'm currently working on this for Core Text rendering, and surprised that this type of information isn't supplied directly (for related graphics like fitted backgrounds/outlines)
These are both works in progress from other stackoverflow questions and my own testing to get perfect bounding boxes (tight)
Common font properties
let leading = floor( CTFontGetLeading(fontCT) + 0.5)
let ascent = floor( CTFontGetAscent(fontCT) + 0.5)
let descent = floor( CTFontGetDescent(fontCT) + 0.5)
var lineHeight = ascent + descent + leading
var ascenderDelta = CGFloat(0)
if leading > 0 {
ascenderDelta = 0
}
else {
ascenderDelta = floor( 0.2 * lineHeight + 0.5 )
}
lineHeight = lineHeight + ascenderDelta
For paragraph styles
var para = NSMutableAttributedString()
// append attributed strings and set NSMutableParagraphStyle
/* ... */
let options : NSStringDrawingOptions = .UsesFontLeading | .UsesLineFragmentOrigin | .UsesDeviceMetrics
let rect = para.boundingRectWithSize(CGSizeMake(fontBoxWidth,10000), options: options, context: nil)
var backgroundBounds = CGRectMake(boundingBox.origin.x + point.x, boundingBox.origin.y + point.y + lineHeight, boundingBox.width, boundingBox.height + ascenderDelta)
For CTFrames
let lines = CTFrameGetLines(frame) as NSArray
let numLines = CFArrayGetCount(lines)
for var index = 0; index < numLines; index++ {
var ascent = CGFloat(0),
descent = CGFloat(0),
leading = CGFloat(0),
width = CGFloat(0)
let line = lines[index] as! CTLine
width = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, &leading))
// adjust with common font property code
var lineOrigin : CGPoint = CGPointMake(0,0)
CTFrameGetLineOrigins(frame, CFRangeMake(index, 1), &lineOrigin)
let bounds = CGRectMake(point.x + lineOrigin.x, point.y + lineOrigin.y - descent, width, ascent + descent)

CATextlayer change size of FONT to fit the frame

I have a CATextlayer of a certain size and NSAttributedString text of unknown length.
I need to adjust the font-size so the text fits the frame (not vice versa :)
Any ideas where to start? :)
[Edit] as nall points out, I can determine the string length, of course, it's some text entered by the user that I need to fit into a box of fixed size.
Working for Swift 5 clean solution.
1) Extend String
extension String {
func size(OfFont font: UIFont) -> CGSize {
return (self as NSString).size(withAttributes: [NSAttributedString.Key.font: font])
}
}
2) Subclass the CATextLayer
class DynamicTextLayer : CATextLayer {
var adjustsFontSizeToFitWidth = false
override func layoutSublayers() {
super.layoutSublayers()
if adjustsFontSizeToFitWidth {
fitToFrame()
}
}
func fitToFrame(){
// Calculates the string size.
var stringSize: CGSize {
get { return (string as? String)!.size(OfFont: UIFont(name: (font as! UIFont).fontName, size: fontSize)!) }
}
// Adds inset from the borders. Optional
let inset: CGFloat = 2
// Decreases the font size until criteria met
while frame.width < stringSize.width + inset {
fontSize -= 1
}
}
}
3) Now go to your code and instead of CATextLayer use DynamicTextLayer
textLayer = DynamicTextLayer()
textLayer?.alignmentMode = .center
textLayer?.fontSize = 40
textLayer?.string = "Example"
textLayer?.adjustsFontSizeToFitWidth = true
I achieved it by doing this:
float fontSize = InitialFontSize;
UIFont *myFont = [UIFont boldSystemFontOfSize:fontSize];
CGSize myFontSize = [YourTextHere sizeWithFont:myFont];
while (myFontSize.width >= MaximunWidth) {
fontSize -= 0.1f;
myFont = [UIFont boldSystemFontOfSize:fontSize];
myFontSize = [YourTextHere sizeWithFont:myFont];
}
CATextLayer *textLayer = [CATextLayer layer];
[textLayer setFrame:CGRectMake(MaximunWidth - myFontSize.width / 2, MaximunHeight - myFontSize.height / 2, myFontSize.width, myFontSize.height)];
[textLayer setFontSize:fontSize];
[textLayer setString:YourTextHere];
[textLayer setAlignmentMode:kCAAlignmentCenter];
I ended up doing this:
textlayer is a CATextlayer
theString is a NSMutableAttributedString
And yes, it's not very elegant and could definitely be improved ;)
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)theString);
CGRect columnRect = CGRectMake(0, 0 , 320, 150);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, columnRect);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
CFRange frameRange = CTFrameGetVisibleStringRange(frame);
int fontSize = 18;
while(theString.string.length > frameRange.length){
fontSize--;
CFStringRef fontName = (__bridge CFStringRef)[defs objectForKey:#"font"];
CTFontRef font = CTFontCreateWithName(fontName, fontSize, NULL);
[theString addAttribute:(NSString *)kCTFontAttributeName
value:(__bridge id)font
range:NSMakeRange(0, theString.string.length)];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)theString);
CGRect columnRect = CGRectMake(0, 0 , 320, 150);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, columnRect);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
frameRange = CTFrameGetVisibleStringRange(frame);
textLayer.string = theString;
}
CATextLayer *textLayer;
[textLayer setWrapped: TRUE];
This will hopefully will work

Multi-line NSAttributedString with truncated text

I need a UILabel subcass with multiline attributed text with support for links, bold styles, etc. I also need tail truncation with an ellipsis. None of the open source code that supports attributed text inside UILabels (TTTAttributedLabel, OHAttributedLabel, TTStyledTextLabel) seem to support tail truncation for multi-line text. Is there an easy way to get this?
Maybe I'm missing something, but whats wrong with? :
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:#"test"];
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
style.lineBreakMode = NSLineBreakByTruncatingTail;
[text addAttribute:NSParagraphStyleAttributeName
value:style
range:NSMakeRange(0, text.length)];
label.attributedText = text;
This works perfectly and will add a ellipsis to the end.
Hi I am the developer of OHAttributedLabel.
There is no easy way to achieve this (as explained in the associated issue I opened on the github repository of my project), because CoreText does not offer such feature.
The only way to do this would be to implement the text layout yourself using CoreText objects (CTLine, etc) instead of using the CTFrameSetter that does this for you (but w/o managing line truncation). The idea would be to build all the CTLines to lay them out (depending on the glyphs in your NSAttributedString it contains and the word wrapping policy) one after the other and manage the ellipsis at the end yourself.
I would really appreciate if someone propose a solution to do this propery as it seems a bit of work to do and you have to manage a range of special/unusual cases too (emoji cases, fonts with odd metrics and unusual glyphs, vertical alignment, take into account the size of the ellipsis itself at the end to know when to stop).
So feel free to dig around and try to implement the framing of the lines yourself it would be really appreciated!
Based on what I found here and over at https://groups.google.com/forum/?fromgroups=#!topic/cocoa-unbound/Qin6gjYj7XU, I came up with the following which works very well.
- (void)drawString:(CFAttributedStringRef)attString inRect:(CGRect)frameRect inContext: (CGContextRef)context
{
CGContextSaveGState(context);
// Flip the coordinate system
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
CGFloat height = self.frame.size.height;
frameRect.origin.y = (height - frameRect.origin.y) - frameRect.size.height ;
// Create a path to render text in
// don't set any line break modes, etc, just let the frame draw as many full lines as will fit
CGMutablePathRef framePath = CGPathCreateMutable();
CGPathAddRect(framePath, nil, frameRect);
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attString);
CFRange fullStringRange = CFRangeMake(0, CFAttributedStringGetLength(attString));
CTFrameRef aFrame = CTFramesetterCreateFrame(framesetter, fullStringRange, framePath, NULL);
CFRelease(framePath);
CFArrayRef lines = CTFrameGetLines(aFrame);
CFIndex count = CFArrayGetCount(lines);
CGPoint *origins = malloc(sizeof(CGPoint)*count);
CTFrameGetLineOrigins(aFrame, CFRangeMake(0, count), origins);
// note that we only enumerate to count-1 in here-- we draw the last line separately
for (CFIndex i = 0; i < count-1; i++)
{
// draw each line in the correct position as-is
CGContextSetTextPosition(context, origins[i].x + frameRect.origin.x, origins[i].y + frameRect.origin.y);
CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, i);
CTLineDraw(line, context);
}
// truncate the last line before drawing it
if (count) {
CGPoint lastOrigin = origins[count-1];
CTLineRef lastLine = CFArrayGetValueAtIndex(lines, count-1);
// truncation token is a CTLineRef itself
CFRange effectiveRange;
CFDictionaryRef stringAttrs = CFAttributedStringGetAttributes(attString, 0, &effectiveRange);
CFAttributedStringRef truncationString = CFAttributedStringCreate(NULL, CFSTR("\u2026"), stringAttrs);
CTLineRef truncationToken = CTLineCreateWithAttributedString(truncationString);
CFRelease(truncationString);
// now create the truncated line -- need to grab extra characters from the source string,
// or else the system will see the line as already fitting within the given width and
// will not truncate it.
// range to cover everything from the start of lastLine to the end of the string
CFRange rng = CFRangeMake(CTLineGetStringRange(lastLine).location, 0);
rng.length = CFAttributedStringGetLength(attString) - rng.location;
// substring with that range
CFAttributedStringRef longString = CFAttributedStringCreateWithSubstring(NULL, attString, rng);
// line for that string
CTLineRef longLine = CTLineCreateWithAttributedString(longString);
CFRelease(longString);
CTLineRef truncated = CTLineCreateTruncatedLine(longLine, frameRect.size.width, kCTLineTruncationEnd, truncationToken);
CFRelease(longLine);
CFRelease(truncationToken);
// if 'truncated' is NULL, then no truncation was required to fit it
if (truncated == NULL)
truncated = (CTLineRef)CFRetain(lastLine);
// draw it at the same offset as the non-truncated version
CGContextSetTextPosition(context, lastOrigin.x + frameRect.origin.x, lastOrigin.y + frameRect.origin.y);
CTLineDraw(truncated, context);
CFRelease(truncated);
}
free(origins);
CGContextRestoreGState(context);
}
Heres a Swift 5 version of #Toydor's answer:
let attributedString = NSMutableAttributedString(string: "my String")
let style: NSMutableParagraphStyle = NSMutableParagraphStyle()
style.lineBreakMode = .byTruncatingTail
attributedString.addAttribute(NSAttributedString.Key.paragraphStyle,
value: style,
range: NSMakeRange(0, attributedString.length))
I haven't tried this in all cases, but something like this could work for truncation:
NSAttributedString *string = self.attributedString;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CFAttributedStringRef attributedString = (__bridge CFTypeRef)string;
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedString);
CGPathRef path = CGPathCreateWithRect(self.bounds, NULL);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
BOOL needsTruncation = CTFrameGetVisibleStringRange(frame).length < string.length;
CFArrayRef lines = CTFrameGetLines(frame);
NSUInteger lineCount = CFArrayGetCount(lines);
CGPoint *origins = malloc(sizeof(CGPoint) * lineCount);
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
for (NSUInteger i = 0; i < lineCount; i++) {
CTLineRef line = CFArrayGetValueAtIndex(lines, i);
CGPoint point = origins[i];
CGContextSetTextPosition(context, point.x, point.y);
BOOL truncate = (needsTruncation && (i == lineCount - 1));
if (!truncate) {
CTLineDraw(line, context);
}
else {
NSDictionary *attributes = [string attributesAtIndex:string.length-1 effectiveRange:NULL];
NSAttributedString *token = [[NSAttributedString alloc] initWithString:#"\u2026" attributes:attributes];
CFAttributedStringRef tokenRef = (__bridge CFAttributedStringRef)token;
CTLineRef truncationToken = CTLineCreateWithAttributedString(tokenRef);
double width = CTLineGetTypographicBounds(line, NULL, NULL, NULL) - CTLineGetTrailingWhitespaceWidth(line);
CTLineRef truncatedLine = CTLineCreateTruncatedLine(line, width-1, kCTLineTruncationEnd, truncationToken);
if (truncatedLine) { CTLineDraw(truncatedLine, context); }
else { CTLineDraw(line, context); }
if (truncationToken) { CFRelease(truncationToken); }
if (truncatedLine) { CFRelease(truncatedLine); }
}
}
free(origins);
CGPathRelease(path);
CFRelease(frame);
CFRelease(framesetter);
Multi Line Vertical Glyph With Truncated. Swift3 and Swift4 version.
Add: Xcode9.1 Swift4 Compatibility. ( use block "#if swift(>=4.0)" )
class MultiLineVerticalGlyphWithTruncated: UIView, SimpleVerticalGlyphViewProtocol {
var text:String!
var font:UIFont!
var isVertical:Bool!
func setupProperties(text: String?, font:UIFont?, isVertical:Bool) {
self.text = text ?? ""
self.font = font ?? UIFont.systemFont(ofSize: UIFont.systemFontSize)
self.isVertical = isVertical
}
override func draw(_ rect: CGRect) {
if self.text == nil {
return
}
// Create NSMutableAttributedString
let attributed = NSMutableAttributedString(string: text) // if no ruby
//let attributed = text.attributedStringWithRuby() // if with ruby, Please create custom method
#if swift(>=4.0)
attributed.addAttributes([
NSAttributedStringKey.font: font,
NSAttributedStringKey.verticalGlyphForm: isVertical,
],
range: NSMakeRange(0, attributed.length))
#else
attributed.addAttributes([
kCTFontAttributeName as String: font,
kCTVerticalFormsAttributeName as String: isVertical,
],
range: NSMakeRange(0, attributed.length))
#endif
drawContext(attributed, textDrawRect: rect, isVertical: isVertical)
}
}
protocol SimpleVerticalGlyphViewProtocol {
}
extension SimpleVerticalGlyphViewProtocol {
func drawContext(_ attributed:NSMutableAttributedString, textDrawRect:CGRect, isVertical:Bool) {
guard let context = UIGraphicsGetCurrentContext() else { return }
var path:CGPath
if isVertical {
context.rotate(by: .pi / 2)
context.scaleBy(x: 1.0, y: -1.0)
path = CGPath(rect: CGRect(x: textDrawRect.origin.y, y: textDrawRect.origin.x, width: textDrawRect.height, height: textDrawRect.width), transform: nil)
}
else {
context.textMatrix = CGAffineTransform.identity
context.translateBy(x: 0, y: textDrawRect.height)
context.scaleBy(x: 1.0, y: -1.0)
path = CGPath(rect: textDrawRect, transform: nil)
}
let framesetter = CTFramesetterCreateWithAttributedString(attributed)
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributed.length), path, nil)
// Check need for truncate tail
if (CTFrameGetVisibleStringRange(frame).length as Int) < attributed.length {
// Required truncate
let linesNS: NSArray = CTFrameGetLines(frame)
let linesAO: [AnyObject] = linesNS as [AnyObject]
var lines: [CTLine] = linesAO as! [CTLine]
let boundingBoxOfPath = path.boundingBoxOfPath
let lastCTLine = lines.removeLast()
let truncateString:CFAttributedString = CFAttributedStringCreate(nil, "\u{2026}" as CFString, CTFrameGetFrameAttributes(frame))
let truncateToken:CTLine = CTLineCreateWithAttributedString(truncateString)
let lineWidth = CTLineGetTypographicBounds(lastCTLine, nil, nil, nil)
let tokenWidth = CTLineGetTypographicBounds(truncateToken, nil, nil, nil)
let widthTruncationBegins = lineWidth - tokenWidth
if let truncatedLine = CTLineCreateTruncatedLine(lastCTLine, widthTruncationBegins, .end, truncateToken) {
lines.append(truncatedLine)
}
var lineOrigins = Array<CGPoint>(repeating: CGPoint.zero, count: lines.count)
CTFrameGetLineOrigins(frame, CFRange(location: 0, length: lines.count), &lineOrigins)
for (index, line) in lines.enumerated() {
context.textPosition = CGPoint(x: lineOrigins[index].x + boundingBoxOfPath.origin.x, y:lineOrigins[index].y + boundingBoxOfPath.origin.y)
CTLineDraw(line, context)
}
}
else {
// Not required truncate
CTFrameDraw(frame, context)
}
}
}
How to use
class ViewController: UIViewController {
#IBOutlet weak var multiLineVerticalGlyphWithTruncated: MultiLineVerticalGlyphWithTruncated! // UIView
let font:UIFont = UIFont(name: "HiraMinProN-W3", size: 17.0) ?? UIFont.systemFont(ofSize: 17.0)
override func viewDidLoad() {
super.viewDidLoad()
let text = "iOS 11 sets a new standard for what is already the world’s most advanced mobile operating system. It makes iPhone better than before. It makes iPad more capable than ever. And now it opens up both to amazing possibilities for augmented reality in games and apps. With iOS 11, iPhone and iPad are the most powerful, personal, and intelligent devices they’ve ever been."
// If check for Japanese
// let text = "すでに世界で最も先進的なモバイルオペレーティングシステムであるiOSに、iOS 11が新たな基準を打ち立てます。iPhoneは今まで以上に優れたものになり、iPadはかつてないほどの能力を手に入れます。さらにこれからはどちらのデバイスにも、ゲームやアプリケーションの拡張現実のための驚くような可能性が広がります。iOS 11を搭載するiPhoneとiPadは、間違いなくこれまでで最もパワフルで、最もパーソナルで、最も賢いデバイスです。"
// if not vertical text, isVertical = false
multiLineVerticalGlyphWithTruncated.setupProperties(text: text, font: font, isVertical: true)
}
}
You may be able to use follow code to have a more simple solution.
// last line.
if (_limitToNumberOfLines && count == _numberOfLines-1)
{
// check if we reach end of text.
if (lineRange.location + lineRange.length < [_text length])
{
CFDictionaryRef dict = ( CFDictionaryRef)attributes;
CFAttributedStringRef truncatedString = CFAttributedStringCreate(NULL, CFSTR("\u2026"), dict);
CTLineRef token = CTLineCreateWithAttributedString(truncatedString);
// not possible to display all text, add tail ellipsis.
CTLineRef truncatedLine = CTLineCreateTruncatedLine(line, self.bounds.size.width - 20, kCTLineTruncationEnd, token);
CFRelease(line); line = nil;
line = truncatedLine;
}
}
I'm using MTLabel in my project and it's a really nice solution for my project.
I integrated wbyoung's solution into OHAttributedLabel drawTextInRect: method if anyone is interested:
- (void)drawTextInRect:(CGRect)aRect
{
if (_attributedText)
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSaveGState(ctx);
// flipping the context to draw core text
// no need to flip our typographical bounds from now on
CGContextConcatCTM(ctx, CGAffineTransformScale(CGAffineTransformMakeTranslation(0, self.bounds.size.height), 1.f, -1.f));
if (self.shadowColor)
{
CGContextSetShadowWithColor(ctx, self.shadowOffset, 0.0, self.shadowColor.CGColor);
}
[self recomputeLinksInTextIfNeeded];
NSAttributedString* attributedStringToDisplay = _attributedTextWithLinks;
if (self.highlighted && self.highlightedTextColor != nil)
{
NSMutableAttributedString* mutAS = [attributedStringToDisplay mutableCopy];
[mutAS setTextColor:self.highlightedTextColor];
attributedStringToDisplay = mutAS;
(void)MRC_AUTORELEASE(mutAS);
}
if (textFrame == NULL)
{
CFAttributedStringRef cfAttrStrWithLinks = (BRIDGE_CAST CFAttributedStringRef)attributedStringToDisplay;
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(cfAttrStrWithLinks);
drawingRect = self.bounds;
if (self.centerVertically || self.extendBottomToFit)
{
CGSize sz = CTFramesetterSuggestFrameSizeWithConstraints(framesetter,CFRangeMake(0,0),NULL,CGSizeMake(drawingRect.size.width,CGFLOAT_MAX),NULL);
if (self.extendBottomToFit)
{
CGFloat delta = MAX(0.f , ceilf(sz.height - drawingRect.size.height))+ 10 /* Security margin */;
drawingRect.origin.y -= delta;
drawingRect.size.height += delta;
}
if (self.centerVertically) {
drawingRect.origin.y -= (drawingRect.size.height - sz.height)/2;
}
}
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, drawingRect);
CFRange fullStringRange = CFRangeMake(0, CFAttributedStringGetLength(cfAttrStrWithLinks));
textFrame = CTFramesetterCreateFrame(framesetter,fullStringRange, path, NULL);
CGPathRelease(path);
CFRelease(framesetter);
}
// draw highlights for activeLink
if (_activeLink)
{
[self drawActiveLinkHighlightForRect:drawingRect];
}
BOOL hasLinkFillColorSelector = [self.delegate respondsToSelector:#selector(attributedLabel:fillColorForLink:underlineStyle:)];
if (hasLinkFillColorSelector) {
[self drawInactiveLinkHighlightForRect:drawingRect];
}
if (self.truncLastLine) {
CFArrayRef lines = CTFrameGetLines(textFrame);
CFIndex count = MIN(CFArrayGetCount(lines),floor(self.size.height/self.font.lineHeight));
CGPoint *origins = malloc(sizeof(CGPoint)*count);
CTFrameGetLineOrigins(textFrame, CFRangeMake(0, count), origins);
// note that we only enumerate to count-1 in here-- we draw the last line separately
for (CFIndex i = 0; i < count-1; i++)
{
// draw each line in the correct position as-is
CGContextSetTextPosition(ctx, origins[i].x + drawingRect.origin.x, origins[i].y + drawingRect.origin.y);
CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, i);
CTLineDraw(line, ctx);
}
// truncate the last line before drawing it
if (count) {
CGPoint lastOrigin = origins[count-1];
CTLineRef lastLine = CFArrayGetValueAtIndex(lines, count-1);
// truncation token is a CTLineRef itself
CFRange effectiveRange;
CFDictionaryRef stringAttrs = CFAttributedStringGetAttributes((BRIDGE_CAST CFAttributedStringRef)_attributedTextWithLinks, 0, &effectiveRange);
CFAttributedStringRef truncationString = CFAttributedStringCreate(NULL, CFSTR("\u2026"), stringAttrs);
CTLineRef truncationToken = CTLineCreateWithAttributedString(truncationString);
CFRelease(truncationString);
// now create the truncated line -- need to grab extra characters from the source string,
// or else the system will see the line as already fitting within the given width and
// will not truncate it.
// range to cover everything from the start of lastLine to the end of the string
CFRange rng = CFRangeMake(CTLineGetStringRange(lastLine).location, 0);
rng.length = CFAttributedStringGetLength((BRIDGE_CAST CFAttributedStringRef)_attributedTextWithLinks) - rng.location;
// substring with that range
CFAttributedStringRef longString = CFAttributedStringCreateWithSubstring(NULL, (BRIDGE_CAST CFAttributedStringRef)_attributedTextWithLinks, rng);
// line for that string
CTLineRef longLine = CTLineCreateWithAttributedString(longString);
CFRelease(longString);
CTLineRef truncated = CTLineCreateTruncatedLine(longLine, drawingRect.size.width, kCTLineTruncationEnd, truncationToken);
CFRelease(longLine);
CFRelease(truncationToken);
// if 'truncated' is NULL, then no truncation was required to fit it
if (truncated == NULL){
truncated = (CTLineRef)CFRetain(lastLine);
}
// draw it at the same offset as the non-truncated version
CGContextSetTextPosition(ctx, lastOrigin.x + drawingRect.origin.x, lastOrigin.y + drawingRect.origin.y);
CTLineDraw(truncated, ctx);
CFRelease(truncated);
}
free(origins);
}
else{
CTFrameDraw(textFrame, ctx);
}
CGContextRestoreGState(ctx);
} else {
[super drawTextInRect:aRect];
}
}
Ran into this issue here in 2021.
I wrapped #Toydor's and #gypsyDev's answer into a UILabel extension so it can be applied where needed. Swift 5.
extension UILabel {
func ensureAttributedTextShowsTruncation() {
guard let text = attributedText else { return }
let attributedString = NSMutableAttributedString(attributedString: text)
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.lineBreakMode = .byTruncatingTail
attributedString.addAttribute(.paragraphStyle,
value: paragraphStyle,
range: NSRange(location: 0, length: attributedString.length))
attributedText = attributedString
}
}
I used as sample MTLabel. It allows to manage line height.
I needed the draw method exactly, so i just put away most stuff i did not need.
This method allows me to draw multilined text in rect with tail truncation.
CGRect CTLineGetTypographicBoundsAsRect(CTLineRef line, CGPoint lineOrigin)
{
CGFloat ascent = 0;
CGFloat descent = 0;
CGFloat leading = 0;
CGFloat width = CTLineGetTypographicBounds(line, &ascent, &descent, &leading);
CGFloat height = ascent + descent;
return CGRectMake(lineOrigin.x,
lineOrigin.y - descent,
width,
height);
}
- (void)drawText:(NSString*) text InRect:(CGRect)rect withFont:(UIFont*)aFont inContext:(CGContextRef)context {
if (!text) {
return;
}
BOOL _limitToNumberOfLines = YES;
int _numberOfLines = 2;
float _lineHeight = 22;
//Create a CoreText font object with name and size from the UIKit one
CTFontRef font = CTFontCreateWithName((CFStringRef)aFont.fontName ,
aFont.pointSize,
NULL);
//Setup the attributes dictionary with font and color
NSDictionary *attributes = [NSDictionary dictionaryWithObjectsAndKeys:
(id)font, (id)kCTFontAttributeName,
[UIColor lightGrayColor].CGColor, kCTForegroundColorAttributeName,
nil];
NSAttributedString *attributedString = [[[NSAttributedString alloc]
initWithString:text
attributes:attributes] autorelease];
CFRelease(font);
//Create a TypeSetter object with the attributed text created earlier on
CTTypesetterRef typeSetter = CTTypesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
//Start drawing from the upper side of view (the context is flipped, so we need to grab the height to do so)
CGFloat y = self.bounds.origin.y + self.bounds.size.height - rect.origin.y - aFont.ascender;
BOOL shouldDrawAlong = YES;
int count = 0;
CFIndex currentIndex = 0;
float _textHeight = 0;
CGContextSaveGState(context);
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, self.bounds.size.height);
CGContextScaleCTM(context, 1.0, -1.0);
//Start drawing lines until we run out of text
while (shouldDrawAlong) {
//Get CoreText to suggest a proper place to place the line break
CFIndex lineLength = CTTypesetterSuggestLineBreak(typeSetter,
currentIndex,
rect.size.width);
//Create a new line with from current index to line-break index
CFRange lineRange = CFRangeMake(currentIndex, lineLength);
CTLineRef line = CTTypesetterCreateLine(typeSetter, lineRange);
//Check to see if our index didn't exceed the text, and if should limit to number of lines
if (currentIndex + lineLength >= [text length])
{
shouldDrawAlong = NO;
}
else
{
if (!(_limitToNumberOfLines && count < _numberOfLines-1))
{
int i = 0;
if ([[[text substringWithRange:NSMakeRange(currentIndex, lineLength)] stringByAppendingString:#"…"] sizeWithFont:aFont].width > rect.size.width)
{
i--;
while ([[[text substringWithRange:NSMakeRange(currentIndex, lineLength + i)] stringByAppendingString:#"…"] sizeWithFont:aFont].width > rect.size.width)
{
i--;
}
}
else
{
i++;
while ([[[text substringWithRange:NSMakeRange(currentIndex, lineLength + i)] stringByAppendingString:#"…"] sizeWithFont:aFont].width < rect.size.width)
{
i++;
}
i--;
}
attributedString = [[[NSAttributedString alloc] initWithString:[[text substringWithRange:NSMakeRange(currentIndex, lineLength + i)] stringByAppendingString:#"…"] attributes:attributes] autorelease];
CFRelease(typeSetter);
typeSetter = CTTypesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
CFRelease(line);
CFRange lineRange = CFRangeMake(0, 0);
line = CTTypesetterCreateLine(typeSetter, lineRange);
shouldDrawAlong = NO;
}
}
CGFloat x = rect.origin.x;
//Setup the line position
CGContextSetTextPosition(context, x, y);
CTLineDraw(line, context);
count++;
CFRelease(line);
y -= _lineHeight;
currentIndex += lineLength;
_textHeight += _lineHeight;
}
CFRelease(typeSetter);
CGContextRestoreGState(context);
}

Resources