In this article, we will briefly explain how UIView encapsulates a simple rich text display CoreTextView. Both pictures and text are created by drawing, so it is the content of this article to understand the drawing process of text and how to insert pictures into text content and realize click events in part of text area.

Let’s take a look at a simple demo implementation

The implementation is to insert an image into the text and delimit click events in a certain text area.

Step 1: Create a custom WSLCoreTextView to achieve text rendering

Put the text drawing task in the – (void)drawRect:(CGRect)rect method in UIView.

The following code

- (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); / / y coordinate inversion CGContextSetTextMatrix (context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM (context, 1.0, 1.0); / / text NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] InitWithString :@" The key to achieving click-highlighting is to get the point of the click-highlighting text area and set whether the recT of the click-highlighting text is in the same area. "] ; [attributeStr addAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:22.f],NSForegroundColorAttributeName:[UIColor  redColor]} range:NSMakeRange(0, attributeStr.length)]; / / draw the scope CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString (CFAttributedStringRef attributeStr); CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, self.bounds); NSInteger length = attributeStr.length; CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, length), path, NULL); // Draw CTFrameDraw(frame, context); / / destroyed CFRelease (frame); CFRelease(path); CFRelease(frameSetter); }Copy the code

Results the following

Step 2: Insert the image in a specific area

After the above action, the text content is drawn in the current View.

To draw a graph within a particular area of text, you first need to understand the components of text height.

Taking Q as an example, first, look at baseline, which is the baseline. The default text is vertical and it is based on baseline. The height above it is ascent, and below it is descent,

[UIFont systemFontOfSize:29].leading;
Copy the code

UIFont has a leading attribute ascent + Descent + leading = lineheight. Since we are drawing a picture, simply set ascent and Descent.

Then you need to figure out how to set these two parameters.

In this time, we need to understand the components of the text. The drawn text can obtain the set of text line number object CFArrayRef in a certain area, which stores the information of the CTLineRef, and each CTLineRef is composed of multiple CTRunrefs. CTRunRef is the basic drawing unit, and the position information bound to these objects is determined after the text is drawn, which can be obtained through calculation. So, what you need to do here is find the text area where you need to replace the image CTRunRef to render the image.

The following code

CTRunDelegateCallbacks callBacks; Memset (&callBacks, 0, sizeof(CTRunDelegateCallbacks)); callBacks.version = kCTRunDelegateCurrentVersion; //ascentCallBacks is ac function that returns the ascent value callbacks. getAscent = ascentCallBacks; //descentCallBacks is a C function that returns the descent value callbacks. getDescent = descentCallBacks; //widthCallBacks is ac function that returns the width of the image callbacks. getWidth = widthCallBacks; / / set the width of the image NSDictionary * info = @ {@ "h" : @ (80), @ "w" : @ (80)}; CTRunDelegateRef Delegate = CTRunDelegateCreate(&callBacks,(__bridge void *)info) Unichar placeHolder = 0xFFFC; unichar placeHolder = 0xFFFC; unichar placeHolder = 0xFFFC; NSString * placeHolderStr = [NSString stringWithCharacters: &placeholder length:1]; NSMutableAttributedString * placeHolderAttrStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr]; / / the proxy binding on new placeholder text CFAttributedStringSetAttribute (CFMutableAttributedStringRef placeHolderAttrStr, CFRangeMake (0, 1), kCTRunDelegateAttributeName, delegate); // Release c object CFRelease(delegate); / / the third position in text insertion picture [attributeStr insertAttributedString: placeHolderAttrStr atIndex: 2];Copy the code

The draw proxy callback c method, whose parameter ref is the value of the parameter bound to the draw proxy object, in this case dic

Ascent static CGFloat ascentCallBacks(void * ref) {return [(NSNumber *)[(__bridge NSDictionary *)ref ValueForKey: @ "h"] floatValue] / 2.0; // Get descent static CGFloat descentCallBacks(void *ref) {return [(NSNumber *)[(__bridge NSDictionary *)ref ValueForKey: @ "h"] floatValue] / 2.0;; Width static CGFloat widthCallBacks(void *ref) {return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"w"] floatValue]; }Copy the code

Ok, with the above code, a string bound to the draw proxy object is added to the original string at the specified location, before the pointing text is drawn.

After drawing, insert the picture,

The following code

CFArrayRef CFArrayRef lines = CTFrameGetLines(frame); CFArrayRef lines = CTFrameGetLines(frame); LineCount = CFArrayGetCount(lines); lineCount = CFArrayGetCount(lines); CGPoint origins[lineCount]; lineCount [lineCount]; CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins); for (int i = 0; i < lineCount; LineRef = CFArrayGetValueAtIndex(lines, I); lineRef = CFArrayGetValueAtIndex(lines, I); CTRunRef set CFArrayRef runs = CTLineGetGlyphRuns(lineRef); CFIndex runCount = CFArrayGetCount(runs); for (int j = 0; j < runCount; CTRunRef runRef = CFArrayGetValueAtIndex(runs, j); NSDictionary * Attributes = (id)CTRunGetAttributes(runRef); If (!) {if (!) {if (!) {if (! attributes) { continue; } // Draw the image, CTRunDelegateRef delegateRef = (__bridge CTRunDelegateRef)[attributes valueForKey:(void) *)kCTRunDelegateAttributeName]; Info = (id)CTRunDelegateGetRefCon(delegateRef); If ([info isKindOfClass:[NSDictionary class]]) {if ([info isKindOfClass:[NSDictionary class]]) {if ([info isKindOfClass:[NSDictionary Class]]) { // Get the starting value of x corresponding to the current string. You can get to a specific point of the characters CGFloat offSetX = CTLineGetOffsetForStringIndex (lineRef, CTRunGetStringRange (runRef). The location, NULL); // Get wide CGFloat w = [info[@"w"] floatValue]; // Get high CGFloat h = [info[@"h"] floatValue]; CGContextDrawImage(context, CGRectMake(Origin. X + offSetX, Origin. Y-40, w, h); [UIImage imageNamed:@"shuaxin"].CGImage); }}}}Copy the code

Results the following

The ascent and Descent adjustments can adjust the way of text and pictures, but in essence, they are drawn by modifying coordinate points.

As shown in figure

Step 3. Implement specific area text click highlight response events

First, create a highlight storage class that stores the text range that needs to be highlighted, the text color backgroundColor, and the actual recT area that can be highlighted by clicking on it.

@interface HightlightAction : NSObject

@property (nonatomic,assign) NSRange range;

@property (nonatomic,strong) UIColor * backgroundColor;

@property (nonatomic,strong) NSMutableArray * hightlightRects;

@end
Copy the code

Implementation idea:

Add ‘Touchbegin’, ‘touchesEnded’ and ‘touchesCancelled’ methods to the current view, record the coordinate points currently clicked, and then compare them. If this is true, redraw the UI and determine if the specified text area is highlighted. If so, the HightlightAction object stores the backgroundColor value to the text in the current region. When the gesture is cancelled or over, redraw the UI again to restore the original state.

The following code

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { UITouch * touch = touches.anyObject; CGPoint p = [touch locationInView:touch.view]; For (int I = 0; i < self.action.hightlightRects.count; i++) { CGRect rect = [self.action.hightlightRects[i] CGRectValue]; Self. isHeightLight = CGRectContainsPoint(rect, p); if (self.isHeightLight) { break; } } //NSLog(@"t.y = %.2f,p.y = %.2f",self.action.hightlightRect.origin.y,p.y); // redraw [self setNeedsDisplay]; } - (void)touch ended :(NSSet< touch *> *)touches withEvent:(UIEvent *)event {self.isHeightLight = NO; // redraw [self setNeedsDisplay]; } - (void)touchesCancelled:(NSSet< uittouch *> *)touches withEvent:(UIEvent *)event {self. IsHeightLight = NO; // redraw [self setNeedsDisplay]; }Copy the code

Now, you need to determine the actual coordinate information of the text that needs to be highlighted, or after the text is drawn, you need to iterate through the CTRunRef inside to get each string that matches the highlighted state, and save the actual coordinate RECT of the text that can be highlighted

We declare and assign a pointer constant to ensure that the value of key is consistent when we bind the string property value.

static NSString * const kHighlightAttributeName = @"wslKHighlightAttributeName";
Copy the code

Here’s a quick word about constant Pointers and pointer constants

The kHighlightAttributeName pointer constant actually limits the address of the object to the right of =.

Create a HightlightAction highlight object as follows:

// Highlight HightlightAction * action = [HightlightAction new]; // Define the highlight color action.backgroundColor = [UIColor greenColor]; Range = NSMakeRange(3, 3); / / this binding need to highlight the text area [attributeStr addAttribute: kHighlightAttributeName value: the action range: the action. The range]; / / add the underlined properties [attributeStr addAttribute: NSUnderlineStyleAttributeName value: @ 1 range: the action. The range]; // Determine whether to highlight according to the status value, To change the text color shows the if (self. IsHeightLight) {[attributeStr addAttribute: NSForegroundColorAttributeName value:action.backgroundColor range:action.range]; }Copy the code

Get and save the actual RECT of the highlighted text area in traversing CTRunRef.

The code is as follows:

/ / access Settings highlight parameter HightlightAction * attrHightlightAction = [attributes valueForKey: kHighlightAttributeName]; If (attrHightlightAction) {// normalOrigin = origin [I]; / / the current text in the current row x offset CGFloat normalOffSetX = CTLineGetOffsetForStringIndex (lineRef, CTRunGetStringRange (runRef). The location, NULL); CGFloat normalAscent; // Upstream high CGFloat normalDescent; // Lower level CGFloat normalLeading; // Get actual value of Ascent Descent Leading, Is used to calculate the coverage area of text CTLineGetTypographicBounds (lineRef, & normalAscent, & normalDescent, & normalLeading); // calculate lineHeight CGFloat lineHeight = normalAscent + ABS(normalDescent) + normalLeading; / / calculate the width CGFloat normalWidth = CTRunGetTypographicBounds (runRef, CFRangeMake (0, 0), NULL, NULL, NULL); / / computed text in the current view on the coordinates of the rect CGRect hightlightRect = CGRectMake (normalOrigin. X + normalOffSetX, the rect. Size. Height - normalOrigin.y - lineHeight, normalWidth, lineHeight); / / save the location information, because need to highlight is a paragraph of text, so it will go repeatedly [attrHightlightAction. HightlightRects addObject: @ (hightlightRect)]; // Temporarily save self.action = attrHightlightAction; }Copy the code

Ok, the actual areas of the text that need to be highlighted have been retrieved, so redraw with the previous Touchbegin, touchesEnded and touchesCancelled methods to achieve a simple text highlight effect.

At this point, the simple rich text display CoreTextView class is complete, and its essence is still in the continuous process of obtaining positions, calculating positions, and adding displays. Don’t laugh at bad code.