In this article, I’m going to use Core Text to handle ellipses in irregular Text,

In a different way,

In this article, I’ll show you how to use Core Text to display exotic Text effects

What’s weird about it?

If you look at the next two pictures,

The descriptive information in the middle of the first sheet, no more than three lines of text, is presented


The description in the middle of the second sheet, more than three lines of text,

Just type an ellipsis and add a full-text expansion button


What’s strange is the irregular shape of the text

The full text expansion button blocks part of the text,

The third line of text displays width narrower than the first two lines


Implementation idea:

It’s a little convoluted,

Get the first two lines of text display width (uI.std.width -cgfloat (16 * 2)),

So let’s figure out a text frame, CTFrame

  • If the text is no more than three lines,

Every line of the text frame is drawn

  • More than three lines of text,

Take the first two lines of the text frame and draw it

Get line 3 of the text frame, get line range,

Find the corresponding text for the third line, new

Get the third line of text display width (uI.std. width -16 left margin -offsetrhs right margin),

Right margin let offsetRhs: CGFloat = 28 button width + 29 button right margin + 10 button left margin + 5 space for ellipsis

Create the second text frame frameInner

Get the first CTLine on the second frameInner and find its corresponding text, subSecond, again through line range

Add ellipsis to subSecond, create CTLine, and draw

Class FrameZeroLabel: UIView{var frameRef: CTFrame? Var contentInfo: String? Var showDot = false init() {super.init(frame: CGRect.zero) backgroundColor = UIColor.white } override func draw(_ rect: CGRect) { guard let ctx = UIGraphicsGetCurrentContext(), let f = frameRef, let content = contentInfo else{ return } let xHigh = bounds.size.height ctx.textMatrix = CGAffineTransform.identity TranslateBy (x: 0, y: xHigh) ctx.scaleBy(x: 1.0, y: -1.0) guard let lines = CTFrameGetLines(f) as? [CTLine] else{ return } let lineCount = lines.count guard lineCount > 0 else { return } let total = max(lineCount, 3) var originsArray = [CGPoint](repeating: CGPoint.zero, count: CTFrameGetLineOrigins(f, CFRangeMake(0, 0), &originsArray) var lastY: CGFloat = 0 var frameY:CGFloat = 0 for i in 0.. <total{ var lineAscent:CGFloat = 0 var lineDescent:CGFloat = 0 var lineLeading:CGFloat = 0 CTLineGetTypographicBounds(lines[i] , &lineAscent, &lineDescent, &lineLeading) var lineOrigin = originsArray[i] switch i { case 0: frameY = lineOrigin.y default: LastY -= 1 frameY = frameY - (lineAscent + lineDescent) Y = frameY} lineOrigin. Y += lastY // Adjust to the desired coordinates CTx. textPosition = lineOrigin // This is the general processing // Switch I {case 0, 1: // Draw first two lines CTLineDraw(lines[I], CTX) default: ShowDot {lineRange = CTLineGetStringRange(lines[I]) let range = NSMakeRange(lineRange.location == kCFNotFound ? NSNotFound : lineRange.location, Linerange.length) let sub = content[range.location..<(range.location + range.length)] let new = String(sub)  /// let page = new.plainX let calculatedSize = page.height(bound: 1000) let offsetRhs: CGFloat = 28 + 29 + 10 + 5 let siZ = CGSize(width: UI.std.width - 16 - offsetRhs, height: CalculatedSize. Height * 3) / / second frame let framesetter = CTFramesetterCreateWithAttributedString let the path = (page) CGPath(rect: CGRect(origin: CGPoint.zero, size: siZ), transform: nil) let frameInner = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil) if let lns = CTFrameGetLines(frameInner) as? [CTLine], lns.count > 0{ let lineRangeSecond = CTLineGetStringRange(lns[0]) let rangeSecond = NSMakeRange(lineRangeSecond.location  == kCFNotFound ? NSNotFound : lineRangeSecond.location, LineRangeSecond. Length) // Find the corresponding text range let subSecond = new[rangeSecond. Location rangeSecond.length)] let newSecond = String(subSecond) + "..." let lnSecond = CTLineCreateWithAttributedString(newSecond.plainX) CTLineDraw(lnSecond, CTX)}} else{// No ellipsis CTLineDraw(lines[I], CTX)}}}}}Copy the code

Supplementary details:

How do you calculate it? Do you have an ellipsis?

Retrieves the list of ctLines from the text frame CTFrame

if let intro = m.introduction{ let page = intro.plainX let calculatedSize = page.height(bound: 3000) let siZ = CGSize(width: UI.std.width - CGFloat( 16 * 2 ), height: CalculatedSize. Height * 3) / / to build the core text text let framesetter = CTFramesetterCreateWithAttributedString let the path = (page)  CGPath(rect: CGRect(origin: CGPoint.zero, size: siZ), transform: Nil) // As mentioned earlier, FrameRef = CTFramesetterCreateFrame(Framesetter, CFRangeMake(0, 0), path, Nil) if let lines = CTFrameGetLines(frameRef) as? [CTLine], lines.count > 2{ TopX_bottomY += 20 top_thebottomy constraint?.constraint. Update (offset: Topx_bottomy.neg) let toHid = (lines.count <= 3) Midtxt.zero.showdot = (toHid == false) expandb.ishidden = toHid // Hide, show more text button //... } / /... }Copy the code

Perfect functions:

Expand button to expand and collapse text

  • 1. Two views are required, one of which is an abridged version, such as the two images above

One is the complete version, easy to understand

Don’t map the

  • 2. The complete rendering of the second view,

Need to get the actual height of the full text frame

Here it is, after rendering, you get it

(Because you can customize the spacing of text when rendering)

class FrameOneLabel: UIView { var frameRef: CTFrame? weak var delegate: DrawDoneProxy? init() { super.init(frame: CGRect.zero) isHidden = true backgroundColor = UIColor.white } override func draw(_ rect: CGRect){ guard let ctx = UIGraphicsGetCurrentContext(), let f = frameRef else{ return } let xHigh = bounds.size.height ctx.textMatrix = CGAffineTransform.identity TranslateBy (x: 0, y: xHigh) ctx.scaleBy(x: 1.0, y: -1.0) guard let lines = CTFrameGetLines(f) as? [CTLine] else{ return } let lineCount = lines.count guard lineCount > 0 else { return } var originsArray = [CGPoint](repeating: CGPoint.zero, count: CTFrameGetLineOrigins(f, CFRangeMake(0, 0), &originsArray) Var lastY: CGFloat = 0 var final: CGFloat = 0 var first: CGFloat? = nil var frameY:CGFloat = 0 for (i,line) in lines.enumerated(){ var lineAscent:CGFloat = 0 var lineDescent:CGFloat = 0 var lineLeading:CGFloat = 0 CTLineGetTypographicBounds(line , &lineAscent, &lineDescent, &lineLeading) var lineOrigin = originsArray[i] switch i { case 0: frameY = lineOrigin.y default: LastY -= 1 frameY = frameY - (lineAscent + lineDescent) Y = frameY} lineOrigin. Y += lastY // Adjust to the desired coordinates ctx.textPosition = lineOrigin CTLineDraw(line, Y} let typoH = lineAscent + lineDescent // The last line of y coordinates final = lineOrigin.y - typoH } let one: CGFloat = first ?? 0 // Let h = one-final // Get the height delegate of the text frame? .done(height: h) } }Copy the code

Review key points:

FrameZeroLabel Importance of the container view

The layout of the container view depends on external changes

The views drawn by CTFrame, especially the second one, must rely on the frames calculated by themselves

  • CTFrameDrawing a view, if it depends on external changes,

It’s easy to stretch and compress

  • FrameZeroLabelContainer view,clipsToBounds

Then you can show it as required

Scene to strengthen

For the above function, add the following effect

  • Height before and after record

  • Initialization effect

Using the previous step, record the height

See GitHub Repo for details

And the original blog


github repo