Multi-line NSAttributedString with truncated text

后端 未结 9 1962
情书的邮戳
情书的邮戳 2020-12-23 14:55

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

相关标签:
9条回答
  • 2020-12-23 15:29

    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)
        }
    }
    
    0 讨论(0)
  • 2020-12-23 15:30

    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.

    0 讨论(0)
  • 2020-12-23 15:30

    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);
    

    }

    0 讨论(0)
提交回复
热议问题