Background:

Generally click a link in the text to jump, you can use UITextView

withUITextViewThe implementation of the

class ViewController: UIViewController, UITextViewDelegate{ @IBOutlet var textView: UITextView! override func viewDidLoad() { super.viewDidLoad() let link = "https://baike.baidu.com/item/%E5%B0%BC%E5%8F%A4%E6%8B%89%E6%96%AF%C2%B7%E5%87%AF%E5%A5%87/1295347?fromtitle=%E5%B0%BC%E 5%8F%A4%E6%8B%89%E6%96%AF%E5%87%AF% e5% A5%87&fromid=415246&fr= Aladdin "Let SRC =" Cage movie, \(link)" let attributedString = NSMutableAttributedString(string: src) attributedString.addAttribute(.link, value: link, range: src.range(ns: link)) textView.attributedText = attributedString } func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) - > Bool {UIApplication. Shared. Open (URL) / / here put interaction events return false}}Copy the code
Disadvantages: not very flexible
  • Apple has its own design, the link automatically turns blue
  • Reading is not friendly, it has to be a link, rightlinkTo decode a percent sign, gg

Not “”. RemovingPercentEncoding

This article describes two implementations using Core Text:

Both use CTFrame to draw the text;

All using a Func touchesBegan to identify events

Implement a

For each click, calculate the index value of the click point in the text and respond if it is in the range of the event

class TextRenderView: UIView { let frameRef:CTFrame let theSize: CGSize // event1 let keyOne = "Willy's Wonderland" // event2 let keyTwo = "Willy's Wonderland" let rawTxt: String let contentPage: Let keyRanges: [Range< string. Index>] Override init(frame: CGRect){ rawTxt = "When his car breaks down, a quiet loner (Nic Cage) agrees to clean an abandoned family fun center in exchange for repairs. He soon finds himself Waging war against possessed animatronic mascots while trapped inside \(keyOne).\n\n When his car broke down, A quiet Lone Ranger (Nic Cage) agrees to clean an abandoned family entertainment center in exchange for repairs. He soon finds himself trapped in \(keyTwo), waging war with the possessed electronic mascot. var tempRanges = [Range<String.Index>]() if let rangeOne = rawTxt.range(of: keyOne){ tempRanges.append(rangeOne) } if let rangeTwo = rawTxt.range(of: keyTwo){ tempRanges.append(rangeTwo) } keyRanges = tempRanges contentPage = NSAttributedString(string: rawTxt, attributes: [NSAttributedString.Key.font: UIFont.regular(ofSize: 15), NSAttributedString.Key.foregroundColor: Uicolor.black]) intrinsic Size let calculatedSize = contentPage.boundingRect(with: CGSize(width: UI.std.width - 15 * 2, height: UI.std.height), options: [.usesFontLeading, .usesLineFragmentOrigin], context: nil).size let padding: CGFloat = 10 theSize = CGSize(width: calculatedSize.width, height: Calculatedsize. height + padding) // Create core text text let framesetter = CTFramesetterCreateWithAttributedString(contentPage) let path = CGPath(rect: CGRect(origin: CGPoint.zero, size: theSize), transform: nil) frameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, nil) super.init(frame: Frame) backgroundColor = uicolor.white} // Draw text override func draw(_ rect: CGRect) { guard let ctx = UIGraphicsGetCurrentContext() else{ return } ctx.textMatrix = CGAffineTransform.identity Ctx.translateby (x: 0, y: bounds.size.height) ctx.scaleby (x: 1.0, y: Override func touch began (_ touches: Set< uittouch >, with event: UIEvent?) { super.touchesBegan(touches, with: event) guard let touch = touches.first else{ return } let pt = touch.location(in: self) guard let offset = parserRect(with: pt, frame: frameRef), let pos = rawTxt.index(rawTxt.startIndex, offsetBy: offset, limitedBy: rawTxt.endIndex) else{ return } if keyRanges[0].contains(pos){ print("000") } else if keyRanges[1].contains(pos){ Func parserRect(with point: CGPoint, frame textFrame: func parserRect(with point: CGPoint, frame textFrame: CTFrame) -> Int? { var result: Int? = nil let path: CGPath = CTFrameGetPath(textFrame) let bounds = path.boundingBox guard let lines = CTFrameGetLines(textFrame) as? [CTLine] else{ return result } let lineCount = lines.count guard lineCount > 0 else { return result } var origins = [CGPoint](repeating: CGPoint.zero, count: LineCount) CTFrameGetLineOrigins(frameRef, CFRangeMake(0, 0), &origins) <lineCount{ let baselineOrigin = origins[i] let line = lines[i] var ascent: CGFloat = 0 var descent: CGFloat = 0 var linegap: CGFloat = 0 let our lineWidth = CTLineGetTypographicBounds (line, & ascent, & descent, & linegap) / / calculate the, Rect let lineFrame = CGRect(x: baselineOrigin. X, y: bounds. height-BaselineOrigin. Y-ascent, width: CGFloat(lineWidth), height: Ascent +descent+linegap + 10) if lineframe. contains(point){// Found, It returns the result = CTLineGetStringIndexForPosition (line, point) break return result}}}}Copy the code
call

Because UIView does not have the intrinsic size of UILabel,

So you need to manually calculate the size required for text rendering and assign it to the frame

class ViewController: UIViewController{

    lazy var content = TextRenderView()
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        content.frame = CGRect(origin: .zero, size: content.theSize)
        content.center = CGPoint(x: UI.std.width / 2, y: UI.std.height / 2)
        view.addSubview(content)

    }



Copy the code

The 2

Calculate index, it is not convenient to expand the hot area of click, especially up and down hot area

The center point of the first text can be calculated and identified within a specified radius from the center point

Paragraph 2 The same

func parserRect(with index: Int, in source: String, frame textFrame: CTFrame) -> CGPoint? { var result: CGPoint? = nil guard let lines = CTFrameGetLines(textFrame) as? [CTLine] else{ return result } let lineCount = lines.count guard lineCount > 0 else { return result } var origins = [CGPoint](repeating: CGPoint.zero, count: lineCount) CTFrameGetLineOrigins(textFrame, CFRangeMake(0, 0), &origins) for i in 0.. <lineCount{ let baselineOrigin = origins[i] let line = lines[i] var ascent: CGFloat = 0 var descent: CGFloat = 0 var linegap: CGFloat = 0 CTLineGetTypographicBounds(line, &ascent, &descent, &linegap) let range = CTLineGetStringRange(line) if range.location + range.length >= index{ let xStart = CTLineGetOffsetForStringIndex(line, index, nil) result = CGPoint(x: baselineOrigin.x+xStart, y: baselineOrigin.y + (ascent - descent)/2) break } } return result } class TextRenderView: UIView { let frameRef:CTFrame let theSize: CGSize // event1 let keyOne = "Willy's Wonderland" // event2 let keyTwo = "Willy's Wonderland" let rawTxt: String let contentPage: NSAttributedString let keyLocation: [Int] let points: [CGPoint?]  override init(frame: CGRect){ rawTxt = "When his car breaks down, a quiet loner (Nic Cage) agrees to clean an abandoned family fun center in exchange for repairs. He soon finds himself Waging war against possessed animatronic mascots while trapped inside \(keyOne).\n\n When his car broke down, A quiet Lone Ranger (Nic Cage) agrees to clean an abandoned family entertainment center in exchange for repairs. He soon finds himself trapped in \(keyTwo), waging war with the possessed electronic mascot. let rangeOne = rawTxt.range(ns: keyOne) let rangeTwo = rawTxt.range(ns: keyTwo) keyLocation = [rangeOne.location + rangeOne.length / 2, rangeTwo.location + rangeTwo.length / 2] contentPage = NSAttributedString(string: rawTxt, attributes: [NSAttributedString.Key.font: UIFont.regular(ofSize: 12), NSAttributedString.Key.foregroundColor: Uicolor.blue]) let calculatedSize = contentPage.boundingRect(with: CGSize(width: UI.std.width - 15 * 2, height: UI.std.height), options: [.usesFontLeading, .usesLineFragmentOrigin], context: nil).size let padding: CGFloat = 10 theSize = CGSize(width: calculatedSize.width, height: CalculatedSize. Height + padding) / / get to render the text frame let framesetter = CTFramesetterCreateWithAttributedString contentPage let  path = CGPath(rect: CGRect(origin: CGPoint.zero, size: theSize), transform: FrameRef = CTFramesetterCreateFrame(Framesetter, CFRangeMake(0, 0), path, nil) Append (parserRect(with: index, in: rawTxt, frame: parserRect(with: index, in: rawTxt, frame: frameRef)) } points = pts super.init(frame: frame) backgroundColor = UIColor.white } override func draw(_ rect: CGRect) { guard let ctx = UIGraphicsGetCurrentContext() else{ return } ctx.textMatrix = CGAffineTransform.identity Ctx.translateby (x: 0, y: bounds.size.height) ctx.scaleby (x: 1.0, y: -1.0) CTFrameDraw(frameRef, CTX) // Render the text above // draw the center point below, If points.count > 0{let path = UIBezierPath() path.lineWidth = 5 for dot in points{if let pt = dot{ctx.move(to: pt) ctx.addLine(to: pt) ctx.setLineCap(.round) ctx.setBlendMode(.normal) ctx.setLineWidth(10) ctx.setStrokeColor(UIColor.magenta.cgColor) ctx.strokePath() } } } } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { super.touchesBegan(touches, with: event) guard let touch = touches.first else{ return } let pt = touch.location(in: Let eventPt = CGPoint(x: pt.x, y: (bounds.size.height - pt.y)) if let one = points[0], one.within(point: Print ("0")} else if let two = points[1], two. Within (point: eventPt){print("1")}}Copy the code
You can also optimize, to allow for possible line breaks, for a few words of the event text, the center point of each word, to figure out

github repo