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 co
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)
}
}
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.
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);
}