1. CoreText basic knowledge

CoreText framework commonly used classes CTFrame, CTFramesetter, CTLine, CTRun, CTRunDelegateRef, CTFont.

The relationship between the classes is shown in the following figure: where, CTRun can be understood as a collection of one or more characters of the same property. (Image from the Internet, here is the use of the following ~)

CTFrame as a whole Canvas (Canvas), which is composed of lines (CTLine), and each line can be divided into one or more small squares (CTRun). Note: You don’t need to create a CTRun yourself, Core Text will automatically create a CTRun based on the NSAttributedString attribute. Each CTRun object has different properties, so you can control the font, color, spacing, and so on. The usual processing steps are: 1. If you use core text, you have a string to display. Then define the style of each part of the string ->attributedString -> generate CTFramesetter -> get CTFrame -> Draw (CTFrameDraw) where you can set the line feed, the alignment, the size of the draw area in more detail. 2. Draw is just a display, click on the event needs a judgment. CTFrame contains multiple CTlines, and you can get the actual position and size of each line. Determines if the click is on a line. CTLine in turn determines the text range at this point (relative to the coordinates of CTLine). It then iterates through all the nstextCheckingResults of the string, determining whether the click was in the rang based on the result’s rang, to get the link and location of the click.

We focus on Ascent, Descent, Baseline, Origin.

2. Draw the text using CoreText

Let’s take a hands-on look at drawing a simple piece of rich text using our CoreText knowledge.

- (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: @ "I am a rich text"]. [attributeStr addAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12], NSForegroundColorAttributeName: [UIColor redColor]} range:NSMakeRange(0, 2)]; // CTFramesetter CTFramesetterRef frameSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attributeStr); CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, self.bounds); // CTFrame NSInteger length = attributeStr.length; CTFrameRef frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, length), path, NULL); // Draw CTFrameDraw(frame, context); CFRelease(frame); CFRelease(path); CFRelease(frameSetter); }Copy the code

Run the project to see a piece of text, of course we can also use a shortcut to draw a piece of text, as follows: This method has to display the View, and has to be able to get the drawing context object. So unload drawRect: as well

- (void)drawRect:(CGRect)rect {[@" I am a text "] drawInRect:rect withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:15], NSForegroundColorAttributeName : [UIColor redColor]}]; }Copy the code

3, the realization of graphic layout

Let’s look at the code first:

- (void)drawRect:(CGRect)rect { // Drawing code CGContextRef context = UIGraphicsGetCurrentContext(); / / affine transformation CGAffineTransform explanation -- > HTTP: / / https://www.jianshu.com/p/6c09d138b31d / / CoreText first coordinate system is set to the in situ, equivalent to a set of translation rotation are invalid. Don't write this sentence here also nothing CGContextSetTextMatrix (context, CGAffineTransformIdentity); // By default, the origin is in the lower left corner, the x axis is in the right direction, and the y axis is in the up direction. CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM(context, 1.0, -1.0); // ContextScalectm (context, 1.0, -1.0); NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString: @ "I am a rich text"]. [attributeStr addAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12], NSForegroundColorAttributeName: [UIColor redColor]} range:NSMakeRange(0, 2)]; CTRunDelegateCallbacks callBacks; memset(&callBacks,0,sizeof(CTRunDelegateCallbacks)); callBacks.version = kCTRunDelegateCurrentVersion; callBacks.getAscent = ascentCallBacks; callBacks.getDescent = descentCallBacks; callBacks.getWidth = widthCallBacks; NSDictionary *info = @{@"w":@130, @"h":@100}; CTRunDelegateRef Delegate = CTRunDelegateCreate(&Callbacks, (__bridge void *)info); / / https://www.sojson.com/hexadecimal.html / / there is an empty string unichar placeHolder = 0 XFFFC; NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1]; NSMutableAttributedString * placeHolderAttrStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr]; CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttrStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate); CFRelease(delegate); [attributeStr insertAttributedString:placeHolderAttrStr atIndex:2]; 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); CTFrameDraw(frame, context); CFArrayRef lines = CTFrameGetLines(frame); CFIndex lineCount = CFArrayGetCount(lines); CGPoint origins[lineCount]; CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins); for (int i = 0; i < lineCount; i++) { CTLineRef lineRef = CFArrayGetValueAtIndex(lines, i); CFArrayRef runs = CTLineGetGlyphRuns(lineRef); CFIndex runCount = CFArrayGetCount(runs); for (int j = 0; j < runCount; j++) { CTRunRef runRef = CFArrayGetValueAtIndex(runs, j); NSDictionary *attributes = (id)CTRunGetAttributes(runRef); if (! attributes) { continue; } CTRunDelegateRef delegateRef = (__Bridge CTRunDelegateRef)[Attributes valueForKey:(id)kCTRunDelegateAttributeName]; if (delegateRef) { CGPoint origin = origins[i]; id info = (id)CTRunDelegateGetRefCon(delegateRef); if ([info isKindOfClass:[NSDictionary class]]) { CGFloat w = [info[@"w"] doubleValue]; CGFloat h = [info[@"h"] doubleValue]; CGFloat offset_x = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL); CGContextDrawImage(context, CGRectMake(origin.x + offset_x, origin.y, w, h), [UIImage imageNamed:@"mababa"].CGImage); } } } } CFRelease(frame); CFRelease(path); CFRelease(frameSetter); } static CGFloat ascentCallBacks(void * ref) { return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"h"] floatValue]; } static CGFloat descentCallBacks(void * ref) { return 0; } static CGFloat widthCallBacks(void * ref) { return [(NSNumber *)[(__bridge NSDictionary *)ref valueForKey:@"w"] floatValue]; }Copy the code

[Image and Text] Effect diagram:

It’s a long code, so I’m going to read it piece by piece.

Setting of rich text placeholders

CTRunDelegateCallbacks callBacks; memset(&callBacks,0,sizeof(CTRunDelegateCallbacks)); callBacks.version = kCTRunDelegateCurrentVersion; callBacks.getAscent = ascentCallBacks; callBacks.getDescent = descentCallBacks; callBacks.getWidth = widthCallBacks; NSDictionary *info = @{@"w":@130, @"h":@100}; CTRunDelegateRef delegate = CTRunDelegateCreate(& callBacks, (__bridge void *)info); / / https://www.sojson.com/hexadecimal.html / / there is an empty string unichar placeHolder = 0 XFFFC; NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1]; NSMutableAttributedString * placeHolderAttrStr = [[NSMutableAttributedString alloc] initWithString:placeHolderStr]; CFAttributedStringSetAttribute((CFMutableAttributedStringRef)placeHolderAttrStr, CFRangeMake(0, 1), kCTRunDelegateAttributeName, delegate); CFRelease(delegate); [attributeStr insertAttributedString:placeHolderAttrStr atIndex:2];Copy the code

So what you do is you insert a placeholder CTRunDelegateRef into the text, and that placeholder is the same as the rest of NSAttributeString, By setting the AttributeString kCTRunDelegateAttributeName to set in. CTRunDelegateRef needs to pass a callbacks that tell the Coretext framework what the ascent, descent and width of the placeholder is. Ascent and Descent are the key to the vertical distribution of images. The key code is as follows:

_width = self.size.width; CGFloat height = self.size.height; If (self. VerticalTextAlignment = = FFVerticalTextAlignmentTop) {/ / and text at the top of the alignment _ascent = self. The font. Ascender; _descent = height - self.font.ascender; } else if (self verticalTextAlignment = = FFVerticalTextAlignmentCenter) {/ / center with text vertically aligned CGFloat fontHeight = self.font.ascender - self.font.descender; CGFloat yOffset = self.font. Ascender - fontHeight * 0.5; _ascent = height * 0.5 + yOffset; _descent = height - _ascent; } else {// align with the bottom of the text _ascent = height; _descent = 0; }Copy the code

The placeholder is by AttributeString said, need to pass in a string, the writing of this paper is introduced into a hex 0 XFFFC, through print placeHolderStr can actually know that he is an empty string, but we are at the time of initialization setting the length of the he is 1, An empty string whose length is equal to 1. It’s perfectly fine for us to pass in a “”.

unichar placeHolder = 0xFFFC;
NSString * placeHolderStr = [NSString stringWithCharacters:&placeHolder length:1];
Copy the code

CTFrameRef created by this Attributestring with the placeholder, and we get that placeholder by making it easier for its CTLineRef, by going through the CTRunRef in each CTLineRef.

Don’t understand can look up CTFrameRef, CTLineRef, CTRunRef relationship diagram.

Printing this AttributeString gives you a clear idea of how the run of the text is distributed.

2021-04-14 18:01:09.107880+0800 fftext-oc [31622:331009] --> I'm {NSColor = "UIExtendedSRGBColorSpace 100 1"; NSFont = "<UICTFont: 0x7f9e3622b320> font-family: \".SFUI-Regular\"; font-weight: normal; font-style: normal; The font - size: 12.00 pt "; } Gym {CTRunDelegate = "<CTRunDelegate 0x600003Aca6A0 [0x7FFF8002E7F0]>"; } A rich text {}Copy the code

Let me mention the function that gets the origin of each row.

CGPoint origins[lineCount]; 
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins);
Copy the code

This function is how you get the origin of each row. The origin is different from the origin of the baseline. In CoreText, the origin is the same as the baseline. That’s the descending height. Different types of text are different in ascending and descending height, and they have their own typesetting display. In my example above, the image appears correctly precisely because we set ascent to the image height and Descent to 0, so origin is aligned with the bottom of the text. If you want to implement the vertical layout of the image in the text, then you need to take care to fix the origin.

CGFloat w = [info[@"w"] doubleValue];
                    CGFloat h = [info[@"h"] doubleValue];
                    CGFloat offset_x = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL);
                    CGContextDrawImage(context, CGRectMake(origin.x + offset_x, origin.y, w, h), [UIImage imageNamed:@"mababa"].CGImage);
Copy the code

Once we get the rect for the delegate and image, we can draw the image directly using the CGContextDrawImage function. Such a simple graphic arrangement is achieved.

4. Insert a custom view into the text

Inserting a custom view is the same as inserting an image in the previous section, we just need to change the code implementation for the final drawing:

UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom]; [button setBackgroundColor:[UIColor redColor]]; [button setTitle: @ "I am a button" forState: UIControlStateNormal]; button.frame = CGRectMake(origin.x + offset_x, rect.size.height - origin.y - h, w, h); [self addSubview:button];Copy the code

(Custom view) Renderings:

5. Link, click and highlight

The key to achieving click-highlight is to get the point of the text area that you clicked on and to set whether the rect of the highlighted text is in the same area.

Let’s look at the code implementation:

- (void)drawRect:(CGRect)rect { // Drawing code CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM (context, 1.0, 1.0); NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString: @ "I am a rich text rich text rich text rich text rich text rich text"].  [attributeStr addAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12], NSForegroundColorAttributeName: [UIColor redColor]} range:NSMakeRange(0, 2)]; // Set click on the highlighted text HightlightAction *action = [HightlightAction New]; action.backgroundColor = [UIColor lightGrayColor]; action.range = NSMakeRange(4, 10); [attributeStr addAttribute:kHighlightAttributeName value:action range:action.range]; / / click to highlight the locale of the underline [attributeStr addAttribute: NSUnderlineStyleAttributeName value: @ 1 range: the action. The range]; // If you find that you are currently clicking on the highlighted area, Then set the background color if under this paragraph (self. TouchHightlight) {[attributeStr addAttribute: NSBackgroundColorAttributeName value:self.touchHightlight.backgroundColor range:self.touchHightlight.range]; } 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); CFArrayRef lines = CTFrameGetLines(frame); CFIndex lineCount = CFArrayGetCount(lines); CGPoint origins[lineCount]; CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins); for (int i = 0; i < lineCount; i++) { CTLineRef lineRef = CFArrayGetValueAtIndex(lines, i); CFArrayRef runs = CTLineGetGlyphRuns(lineRef); CFIndex runCount = CFArrayGetCount(runs); for (int j = 0; j < runCount; j++) { CTRunRef runRef = CFArrayGetValueAtIndex(runs, j); NSDictionary *attributes = (id)CTRunGetAttributes(runRef); if (! attributes) { continue; } // Fetch the highlighted model from run and calculate its rect HightlightAction *action = Attributes [kHighlightAttributeName]; if (! action) { continue; } CGPoint origin = origins[i]; CGFloat offset_x = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL); CGFloat width = CTRunGetTypographicBounds(runRef, CFRangeMake(0, 0), NULL, NULL, NULL); CGFloat ascent; // CGFloat descent; // CGFloat leading; / / line spacing CTLineGetTypographicBounds (lineRef, & ascent, & descent, & leading); CGFloat glyphHeight = ascent + ABS(descent); CGFloat lineHeight = glyphHeight + leading; CGRect hightlightRect = CGRectMake(origin. X + offset_x, rect.size.height - origin. lineHeight); action.hightlightRect = hightlightRect; self.action = action; } } CTFrameDraw(frame, context); CFRelease(frame); CFRelease(path); CFRelease(frameSetter); } // start tapping - (void)touches began :(NSSet< touches *> *)touches withEvent:(UIEvent *) {touches *touch = touches.anyObject; CGPoint p = [touch locationInView:touch.view]; If (CGRectContainsPoint (self. Action. HightlightRect, p)) {/ / save the currently selected self. The highlight model touchHightlight = self. The action; [self setNeedsDisplay]; }} self.touchTouchSize.touches (NSSet< touches *> *) Touches withEvent (UIEvent *) {self.touchTouchSize.touches = nil; [self setNeedsDisplay]; } self.touchHighlight = nil;} self.touchhighlight = nil; (void) cancelled (NSSet< touches *> *) Touches withEvent (UIEvent *)event {self.touchHighlight = nil; [self setNeedsDisplay]; }Copy the code

Once the code runs, you can see that you’ve implemented the basic click-text highlighting effect.

As you can see, the way you do that is by finding all the runs in the line that have the corresponding highlighted runs. And then you just get its rect.

CGPoint origin = origins[i]; CGFloat offset_x = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL); CGFloat width = CTRunGetTypographicBounds(runRef, CFRangeMake(0, 0), NULL, NULL, NULL); CGFloat ascent; // CGFloat descent; // CGFloat leading; / / line spacing CTLineGetTypographicBounds (lineRef, & ascent, & descent, & leading); CGFloat glyphHeight = ascent + ABS(descent); CGFloat lineHeight = glyphHeight + leading; CGRect hightlightRect = CGRectMake(origin. X + offset_x, rect.size.height - origin. lineHeight);Copy the code

For the above text, we print all runs in this line, and we can know that the number of runs is 4, which are “I am”, “one”, “rich text rich text rich text”, “text rich text rich text”. The text in each run is the same attribute. So imagine if we inserted a text with a different property in the middle of the highlighted area of the appeal.

Try adding the following code, and then run the above code again to see if clicking highlight works.

NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] initWithString: @ "I am a rich text rich text rich text rich text rich text rich text"]. [attributeStr addAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12], NSForegroundColorAttributeName: [UIColor redColor]} range:NSMakeRange(0, 2)]; / / let the sixth character fonts bigger [attributeStr addAttribute: NSFontAttributeName value: [UIFont systemFontOfSize: 20] range: NSMakeRange (13, 1)]; // The rest of the code...Copy the code

After running, you will find that the click is invalid! What’s going on here?

In fact, there is no failure, click on the last large font of the rich or can be highlighted.

Let’s print the calculated hightlightRect and find that the width is only 20 and the x is wrong.

(origin = (x = 156.50400000000002, y = -2.8800000000000026), size = (width = 20.37999999999999999, y = -2.8800000000000026), size = (width = 20.37999999999999999, y = -2.8800000000000026), Height = 21.880000000000003))

Then we count how many runs there are, and it turns out to be five. Our new font property truncates the highlight property, resulting in two runs containing kHighlightAttributeName in the for loop, and the second one with the large size “rich”. So the final calculated width is the width of the rich word, and x is the x-coordinate of the second run. Clicking on other words does not highlight the effect.

The solution is simply to sum the widths of the runs at both ends, using the x coordinate of the first run as the value of x.

CGFloat hightlightX = 0; CGFloat hightlightWidth = 0; int effectRunCount = 0; for (int i = 0; i < lineCount; i++) { CTLineRef lineRef = CFArrayGetValueAtIndex(lines, i); CFArrayRef runs = CTLineGetGlyphRuns(lineRef); CFIndex runCount = CFArrayGetCount(runs); NSLog(@"runcount ---> %ld",runCount); for (int j = 0; j < runCount; j++) { CTRunRef runRef = CFArrayGetValueAtIndex(runs, j); NSDictionary *attributes = (id)CTRunGetAttributes(runRef); if (! attributes) { continue; } HightlightAction *action = attributes[kHighlightAttributeName]; if (! action) { hightlightWidth = 0; continue; } effectRunCount += 1; CGPoint origin = origins[i]; CGFloat offset_x = CTLineGetOffsetForStringIndex(lineRef, CTRunGetStringRange(runRef).location, NULL); CGFloat width = CTRunGetTypographicBounds(runRef, CFRangeMake(0, 0), NULL, NULL, NULL); if (effectRunCount == 1) { hightlightX = origin.x + offset_x; } hightlightWidth += width; CGFloat ascent; // CGFloat descent; // CGFloat leading; / / line spacing CTLineGetTypographicBounds (lineRef, & ascent, & descent, & leading); CGFloat glyphHeight = ascent + ABS(descent); CGFloat lineHeight = glyphHeight + leading; // CGRect hightlightRect = CGRectMake(hightlightX, rect.size.height-origin. Y-lineheight, hightlightWidth, lineHeight); action.hightlightRect = hightlightRect; self.action = action; }}Copy the code

This will allow the highlight to appear normally.

6, custom truncation

To implement a custom truncation, we need to switch our thinking and take a look at the code above. Drawing is done by calling the function CTFrameDraw. But to achieve something similar to adding… Or something custom, so we need to draw by line, by calling the function CTLineDraw.

Let’s look at the code first.

- (void)drawRect:(CGRect)rect { // Drawing code CGContextRef context = UIGraphicsGetCurrentContext(); CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextTranslateCTM(context, 0, self.bounds.size.height); CGContextScaleCTM (context, 1.0, 1.0); NSMutableAttributedString * attributeStr = [[NSMutableAttributedString alloc] InitWithString: @ "I'm a rich text I am a rich text I'm a rich text I am a rich text I'm a rich text I am a rich text I'm a rich text I am a rich text I'm a rich text I am a rich text I'm a rich text I am a rich text I'm a rich text I am a rich text a rich I am Text I am a rich text I am a rich text I am a rich text "]; [attributeStr addAttributes:@{NSFontAttributeName : [UIFont systemFontOfSize:12], NSForegroundColorAttributeName: [UIColor redColor]} range:NSMakeRange(0, 2)]; 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); CFArrayRef lines = CTFrameGetLines(frame); CFIndex lineCount = CFArrayGetCount(lines); CGPoint origins[lineCount]; CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), origins); NSInteger numberOfLines = 1; NSInteger count = numberOfLines == 0 ? lineCount : numberOfLines; if (count > lineCount) count = lineCount; // Determine whether the truncation is required. If the number of rows is less than the total number of rows, the truncation is required. BOOL needShowTruncation = count < lineCount; for (int i = 0; i < count; i++) { CTLineRef lineRef = CFArrayGetValueAtIndex(lines, i); CFRange range = CTLineGetStringRange(lineRef); CGPoint origin = origins[i]; // Once we have drawn text through CTLineDraw, we need to set the position of the lines ourselves, otherwise they will be displayed at the bottom. CGContextSetTextPosition(context, origin.x, origin.y); if (i == 0 && needShowTruncation) { NSMutableAttributedString *drawLineString = [[NSMutableAttributedString alloc] initWithAttributedString:[attributeStr attributedSubstringFromRange:NSMakeRange(range.location, range.length)]]; NSAttributedString *truncationTokenText = [[NSAttributedString alloc] initWithString:@" Click on me to expand" attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:15], NSForegroundColorAttributeName: [UIColor brownColor]}]; CTLineTruncationType type = kCTLineTruncationEnd; // These two lines control whether the truncation is placed at the beginning, middle, or end. [drawLineString appendAttributedString:truncationTokenText]; CTLineRef drawLineRef = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)drawLineString); CTLineRef tokenLineRef = CTLineCreateWithAttributedString((__bridge CFAttributedStringRef)truncationTokenText); / / create a line with a truncation operator CTLineRef truncationLineRef = CTLineCreateTruncatedLine (drawLineRef, the rect. Size. Width, type, tokenLineRef); CFRelease(drawLineRef); CFRelease(tokenLineRef); // Draw CTLineDraw(truncationLineRef, context); } else { CTLineDraw(lineRef, context); } } CFRelease(frame); CFRelease(path); CFRelease(frameSetter); }Copy the code

The result is as follows.

We can also achieve the effect of clicking “More” to expand all the text, so the logic and the text are highlighted in the same way, which we won’t explain here.

7, expand

  • Asynchronous rendering

  • Long press to select text

  • Fuzzy constraint


For those interested, see my encapsulation implementation of Coretext at 👉👉👉👉FFText

Reference blog:

The Core Text feature

CoreText Introduction – Advanced

CoreText system framework

Concept of typography

CoreText basic usage

CoreText to achieve graphic layout