Step 1: Basics

Achieve the following effect, noting that each line is spaced 0 apart

1. There is such a rich text
Let attributed = NSMutableAttributedString (string: "xinfeng wine bucket ShiQian, xianyang rangers how many years. \n Meet spirit for you to drink, the horse high-rise weeping willows edge. N born shi Han Yu Lin Lang, at the beginning of the fight with the hussar Yu Yang. N Shuzhi does not bitter to bian Court, longitudinal death still smell xiagu xiang." ); attributed.addAttributes([NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20)], range: NSMakeRange(0, attributed.length)) attributed.addAttributes([NSAttributedString.Key.font: UIFont.systemFont(ofSize: 30)], range: NSMakeRange(2, 2)) attributed.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.orange], range: NSMakeRange(0, 7)) attributed.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.red], range: NSMakeRange(8, 7))Copy the code
2. On top, draw a line
        let strokePath = CGMutablePath()
        strokePath.addRect(CGRect(x: 1, y: 1, width: UIScreen.main.bounds.width-2, height: 1))
        ctx.addPath(strokePath)
        ctx.setStrokeColor(UIColor.purple.cgColor)
        ctx.strokePath()
Copy the code

The coordinate system for CoreText, the origin is in the lower left,

UIKit coordinates, the origin is on the upper left

Three, let’s take a frame, flip it
let xHigh = bounds.size.height ctx.textMatrix = CGAffineTransform.identity ctx.translateBy(x: 0, y: XHigh) ctx.scaleby (x: 1.0, y: -1.0)Copy the code
4. Display text

The process is,

Create a CTFrame with rich text,

Take CTFrame and get the set of CTLines inside

So let’s draw each CTLine

TextPosition sets the draw origin for each row

The height of each line is lineAscent + lineDescent + lineLeading

Draw a line, y value of origin – (lineAscent + lineDescent),

If I draw the second line, I ignore lineLeading

        let path = CGMutablePath()
        path.addRect(bounds)
           let ctFrameSetter = CTFramesetterCreateWithAttributedString(attributed)
           let ctFrame = CTFramesetterCreateFrame(ctFrameSetter, CFRangeMake(0, attributed.length), path, nil)
           let lines = CTFrameGetLines(ctFrame) as NSArray
           var originsArray = [CGPoint](repeating: CGPoint.zero, count: lines.count)
           CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, 0), &originsArray)
           var frameY:CGFloat              = 0
           for (i,line) in lines.enumerated() {
               var lineAscent:CGFloat      = 0
               var lineDescent:CGFloat     = 0
               var lineLeading:CGFloat     = 0
               CTLineGetTypographicBounds(line as! CTLine, &lineAscent, &lineDescent, &lineLeading)
               var lineOrigin = originsArray[i]
               
               if i > 0{
                   frameY = frameY - lineAscent - lineDescent
                   lineOrigin.y = frameY
               }
               else{
                   frameY = lineOrigin.y
               }
               ctx.textPosition = lineOrigin
               CTLineDraw(line as! CTLine, ctx)
           }
Copy the code

github repo

Step 2: Improve

I’m going to do the following, I have a text field,

In the vertical direction, spacing is strictly defined

A, way of thinking

It’s a familiar rich text, drawn on UIView

Maybe there’s too much rendering to fit on the screen,

Render view, placed on a UIScroll

  • Take the familiar rich text, calculate a render size big enough,

Specify the content size of the superview,

Take this size and create the Frame

  • Then you customize the drawing, specify the origin of each row,

Once the drawing is complete, the actual content size is calculated

Call a callback that specifies the content size of the superview

B, to achieve

First calculate enough size to render,

The size here is 3 times the height, calculatedSize. Height * 3

Create CTFrame

var contentPage: NSAttributedString? {didSet{guard let page = contentPage else{return} // Calculate text box size, Because UIView does not have the intrinsic size of UILabel let widthInUse = UI.std.width - textContentConconst. Padding * 2 let calculatedSize = page.boundingRect(with: CGSize(width: widthInUse, height: 3000), options: [.usesFontLeading, .usesLineFragmentOrigin], context: nil).size let siZ = CGSize(width: widthInUse, 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) frameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil) s = siZ } }Copy the code
The drawing part
  • So let’s do the familiar coordinate system flip

Then prepare to render each line

// Get guard let lines = CTFrameGetLines(f) as? [CTLine] else{ return } let lineCount = lines.count guard lineCount > 0 else { return } var frameY:CGFloat = 0 var originsArray = [CGPoint](repeating: CGPoint.zero, count: CTFrameGetLineOrigins(f, CFRangeMake(0, 0), &originsArray) Var final: CGFloat = 0 var final: CGFloat = 0 var final: CGFloat = 0 var first: CGFloat? = nilCopy the code

Render each line

  • The point is, if I take the y-coordinate of row 2,

(The coordinates of the first row, do not deal with)

Coordinates of line 2 – line height of first line (lineAscent + lineDescent) – the spacing specified

  • And as for the Y coordinate on the NTH row,

Coordinates of row (n-1) – Row height (lineAscent + lineDescent) of row (n-1) – Accumulated spacing (lastY)

  • The x coordinate, most of which are simple, the ones with boxes are specified as constants

In pinyin, calculating travel wide, TextContentConst fBgTypoImg. Width – CGFloat half (sentenceW)

for (i,line) in lines.enumerated(){ var lineAscent:CGFloat = 0 var lineDescent:CGFloat = 0 var lineLeading:CGFloat = 0 // Get the width of the current row, A place to house pinyin let sentenceW = CTLineGetTypographicBounds (line, & lineAscent, & lineDescent, &lineLeading) var lineOrigin = originsArray[i] lineOrigin.x = TextContentConst.padding if info.eightY.contains(i){ lastY  -= 8 } else{ switch i { case 1: lastY -= TextContentConst.padding default: lastY -= 20 } } if info.pronounceX.contains(i){ let makeUp = TextContentConst.fBgTypoImg.width - CGFloat(sentenceW) lineOrigin.x += makeUp / 2 } switch i { case 0: frameY = lineOrigin.y default: frameY = frameY - (lineAscent + lineDescent) lineOrigin.y = frameY } lineOrigin.y += lastY let yOffset = lineOrigin.y - lineDescent - 20 if i == 0{ ctx.draw(line: yOffset) } ctx.textPosition = lineOrigin if info.contains(pair: // drawPairs(context: CTX, ln: line, startPoint: lineOrigin, ascent: LineAscent)} else{// draw a line CTLineDraw(line, Y} let typoH = lineAscent + lineDescent final = lineOrigin. Y -  typoH }Copy the code
  • Draw, framed pairs

CTLine, which contains several CTRUNs,

It uses CTLineDraw,

CTRunDraw is used here

CTRunDraw(run, ctx, CFRange(location: 0, length: 0))

Here CFRange(location: 0, length: 0) is drawing all

First draw the image below, (the calculation is a bit convoluted), and then draw the text above

The x coordinate, which is pretty straightforward, is a constant

func drawPairs(context ctx: CGContext, ln line: CTLine,startPoint lineOrigin: CGPoint, ascent lineAscent: CGFloat){ if let pieces = CTLineGetGlyphRuns(line) as? [CTRun]{ let pieceCnt = pieces.count var zeroP = lineOrigin zeroP.y -= 5 for j in 0.. <pieceCnt{ switch j { case 0: var frame = TextContentConst.fBgTypoImg frame.origin.y = lineOrigin.y + lineAscent - TextContentConst. FBgTypoImg. Size, height + TextContentConst offsetP. Y / / draw the image below bgGrip? .draw(in: frame) zeroP.x += TextContentConst.offsetP.x case 1: zeroP.x = 92 default: CTRunDraw(pieces[j], CTX, CFRange(location: 0, length: 0))}}Copy the code

There is no SceneDelegate project, CTLine contains two CTRun, logic is relatively clear

SceneDelegate project, CTLine contains 5 CTRun, effect is ok, logic ha ha

github repo

Enhanced, one line of multiple boxes

You can see, on the bottom row, multiple boxes

The effect, based on the above,

The difference is,

In this line box, each word is a rich text, and a rich text is a CTLine

CTRun draw at range

Ctx.textposition = lineOrigin if Info. contains(pair: I){// drawPairs(context: CTX, ln: line, startPoint: lineOrigin, ascent: lineAscent) } else if info.phraseY.contains(i), Let lnHeight = lineAscent + lineDescent + lineLeading lastY -= let lnHeight = lineAscent + lineLeading lastY -= drawGrips(m: info, lnH: lnHeight, index: i, dB: startIdx, lineOrigin: lineOrigin, context: ctx, lnAscent: LineAscent)} else{// Draw a line of text CTLineDraw(line, CTX)}Copy the code

Concrete line box, implementation

Notice that the CTLine of the original CTFrame is discarded

The coordinates of CTLine obtained originally are retained for reference

The space between the original lines. It becomes the spacing between the cells.

So return half of an interval ((cell height – row height), accumulated to the historical interval (lastY)

The following coordinate calculation, more around

func drawGrips(m info: TxtRenderInfo, lnH lnHeight: CGFloat, index i: Int, dB startIdx: Int, lineOrigin lnOrigin: CGPoint, context ctx: CGContext, lnAscent lineAscent: STRS [i-startidx] let glyphCount = content.count var frameImg = TextContentConst. FBgTypoImg let lnOffsset = (TextContentConst padding - lnHeight) * 0.5 var lineOrigin = lnOrigin Y -= lnOffsset var textP = lineOrigin // Handle this line of text, each word for idx in 0.. <glyphCount{let pieX = String(content[idx]) Become a CTLine let ln = CTLineCreateWithAttributedString (pieX. Word) let lnSize. = ln lnSize let typeOriginX = Textcontentconconst. Padding * CGFloat(IDx + 1) Textp. x = typeOriginX + (TextContentConconst. Padding-lnsize.width) * 0.5 ctx.textPosition = textP frameImg.origin.x = typeOriginX frameImg.origin.y = lineOrigin.y + lineAscent - TextContentConst. FBgTypoImg. The size, height + TextContentConst offsetP. Y / / draw the background picture below bgGrip? .draw(in: frameImg) // Draw the text above CTLineDraw(ln, CTX)} return lnOffsset}Copy the code

github repo