前言
這是一個移動端快速發展的時代,不管你承不承認,作為一個app開發者,社交屬性總是或多或少出現在我們開發的業務需求中,其中作為IM最重要的組成元素——表情,如何進行文字和表情混合編程是一門重要的技術。
本文將使用iOS中的coreText框架來完成我們的圖文混編之旅,除此之外,還實現文本超鏈接效果。在開始本篇的代碼之前,我們先通過iOS框架結構圖來了解CoreText所處的位置:
iOS框架結構圖
coreText基礎
首先我們要知道圖文混編的原理 —— 在需要顯示圖片的文本位置使用特殊的字符顯示,然後在繪制這些文本的將圖片直接繪制顯示在這些特殊文本的位置上。因此,圖文混編的任務離不開一個重要的角色——NSAttributedString。
這個對比NSString多了各種類似粗斜體、下劃線、背景色等文本屬性的NSAttributedString,每個屬性都有其對應的字符區域。這意味著你可以將前幾個字符設置為粗體,而後面的字符為斜體且帶著下劃線。在iOS6之後已經有能夠設置控件的富文本屬性了,但如果想要實現我們的圖文混編,我們需要使用coreText來對屬性字符串進行繪制。在coreText繪制字符的過程中,最重要的兩個概念是CTFramesetterRef跟CTFrameRef,他們的概念如下:
繪制關系
在創建好要繪制的富文本字符串之後,我們用它來創建一個CTFramesetterRef變量,這個變量可以看做是CTFrameRef的一個工廠,用來輔助我們創建後者。在傳入一個CGPathRef的變量之後我們可以創建相應的CTFrameRef然後將富文本渲染在對應的路徑區域內。這段創建代碼如下(由於coreText基於C語言的庫,所有對象都需要我們手動釋放內存):
CGContextRef ctx = UIGraphicsGetCurrentContext(); NSAttributedString * content = [[NSAttributedString alloc] initWithString: @"這是一個測試的富文本,這是一個測試的富文本"]; CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content); CGMutablePathRef paths = CGPathCreateMutable(); CGPathAddRect(paths, NULL, CGRectMake(0, 0, 100, 100)); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, content), paths, NULL); CTFrameDraw(frame, ctx);//繪制文字 // 釋放內存 CFRelease(paths); CFRelease(frame); CFRelease(framesetter);
除此之外,每一個創建的CTFrameRef中存在一個或者更多的CTLineRef變量,這個變量表示繪制文本中的每一行文本。每個CTLineRef變量中存在一個或者更多個CTRunRef變量,在文本繪制過程中,我們並不關心CTLineRef或者CTRunRef變量具體對應的是什麼字符,這些工作在更深層次系統已經幫我們完成了創建。創建過程的圖如下:
coreText對象模型
通過CTFrameRef獲取文本內容的行以及字符串組的代碼如下:
CFArrayRef lines = CTFrameGetLines(frame); CGPoint lineOrigins[CFArrayGetCount(lines)]; CTFrameGetLineOrigins(_frame, CFRangeMake(0, 0), lineOrigins); for (int idx = 0; idx < CFArrayGetCount(lines); idx++) { NSLog(@"第%d行起始坐標%@", idx, NSStringFromCGPoint(lineOrigins[idx])); CTLineRef line = CFArrayGetValueAtIndex(lines, idx); CFArrayRef runs = CTLineGetGlyphRuns(line); CGFloat runAscent; CGFloat runDescent; CGFloat runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &runAscent, &runDescent, NULL); NSLog(@"第%d個字符組的寬度為%f", idx, runWidth); }
圖文混編的做法就是在我們需要插入表情的富文本位置插入一個空字符占位,然後實現自定義的CTRunDelegateCallbacks來設置這個占位字符的寬高位置信息等,為占位字符添加一個自定義的文本屬性用來存儲對應的表情圖片名字。接著我們通過CTFrameRef獲取渲染的文本行以及文本字符,判斷是否存在存儲的表情圖片,如果是就將圖片繪制在占位字符的位置上。
富文本插入表情
下面代碼是創建一個CTRunDelegate的代碼,用來設置這個字符組的大小尺寸:
void RunDelegateDeallocCallback(void * refCon) {} CGFloat RunDelegateGetAscentCallback(void * refCon) { return 20; } CGFloat RunDelegateGetDescentCallback(void * refCon) { return 0; } CGFloat RunDelegateGetWidthCallback(void * refCon) { return 20; } CTRunDelegateCallbacks imageCallbacks; imageCallbacks.version = kCTRunDelegateVersion1; imageCallbacks.dealloc = RunDelegateDeallocCallback; imageCallbacks.getWidth = RunDelegateGetWidthCallback; imageCallbacks.getAscent = RunDelegateGetAscentCallback; imageCallbacks.getDescent = RunDelegateGetDescentCallback; CTRunDelegateRef runDelegate = CTRunDelegateCreate(&imageCallbacks, "這是回調函數的參數");
文字排版
文字排版屬於又臭又長的理論概念,但是對於我們更好的使用coreText框架,這些理論知識卻是不可缺少的。
字體
與我們所認知的字體不同的是,在計算機中字體指的是一系列的相同樣式、相同大小的字形的集合,即14號宋體跟15號宋體在計算機看來是兩種字體。而我們所說的字體指 宋體 / 楷體 這些字體類型
字符與字形
文本排版的過程實際上是從字符到字形之間的轉換。字符表示的是文字本身的信息意義,而字形表示的是這個文字的圖形表現格式。同一個字符由於大小、體形之間的差別,存在著不同字形。由於連寫的存在,多個字符可能只對應一個字形:
連寫對應單個字形
字形描述集
描述了字形表現的多個參數,包括:
1、邊框(Bounding box):一個假想的邊框,盡可能的將整個字形容納
2、基線(Baseline):一條假想的參考線,以此為基礎渲染字形。正常來說字母x、m、s最下方的位置就是參考線所在y坐標
3、基礎原點(Origin):基線最左側的坐標點
4、行間距(Leading):行與行之間的間距
5、字間距(Kerning):字與字之間的間距
6、上行高度(Ascent):字形最高點到基線的距離,正數。同一行取字符最大的上行高度為該行的上行高度
7、下行高度(Descent):字形最低點到基線的距離,負數。同一行取字符最小的下行高度為該行的下行高度
字形描述屬性
下圖中綠色線條表示基線,黃色線條表示下行高度,綠色線條到紅框最頂部的距離為上行高度,而黃色線條到紅框底部的距離為行間距。因此行高的計算公式是lineHeight = Ascent + |Descent| + Leading
字符描述屬性
圖文混編
前文講了諸多的理論知識,終於來到了實戰的階段,先放上本文的demo地址和效果圖:
demo演示
由於富文本的繪制需要用到一個CGContextRef類型的上下文,那麼創建一個繼承自UIView的自定義控件並且在drawRect:方法中完成富文本繪制是最方便的方式,我給自己創建的類命名為LXDTextView
在CoreText繪制文本的時候,坐標系的原點位於左下角,因此我們需要在繪制文字之前對坐標系進行一次翻轉。並且在繪制富文本之前,我們需要構建好渲染的富文本並在方法裡返回:
- (NSMutableAttributedString *)buildAttributedString { //創建富文本,並且將超鏈接文本設置為藍色+下劃線 NSMutableAttributedString * content = [[NSMutableAttributedString alloc] initWithString: @"這是一個富文本內容"]; NSString * hyperlinkText = @"@這是鏈接"; NSRange range = NSMakeRange(content.length, hyperlinkText.length); [content appendAttributedString: [[NSAttributedString alloc] initWithString: hyperlinkText]]; [content addAttributes: @{ NSForegroundColorAttributeName: [UIColor blueColor] } range: range]; [content addAttributes: @{ NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle) } range: range]; //創建CTRunDelegateRef並設置回調函數 CTRunDelegateCallbacks imageCallbacks; imageCallbacks.version = kCTRunDelegateVersion1; imageCallbacks.dealloc = RunDelegateDeallocCallback; imageCallbacks.getWidth = RunDelegateGetWidthCallback; imageCallbacks.getAscent = RunDelegateGetAscentCallback; imageCallbacks.getDescent = RunDelegateGetDescentCallback; NSString * imageName = @"emoji"; CTRunDelegateRef runDelegate = CTRunDelegateCreate(&imageCallbacks, (__bridge void *)imageName); //插入空白表情占位符 NSMutableAttributedString * imageAttributedString = [[NSMutableAttributedString alloc] initWithString: @" "]; [imageAttributedString addAttribute: (NSString *)kCTRunDelegateAttributeName value: (__bridge id)runDelegate range: NSMakeRange(0, 1)]; [imageAttributedString addAttribute: @"imageNameKey" value: imageName range: NSMakeRange(0, 1)]; [content appendAttributedString: imageAttributedString]; CFRelease(runDelegate); return content; } - (void)drawRect: (CGRect)rect { //獲取圖形上下文並且翻轉坐標系 CGContextRef ctx = UIGraphicsGetCurrentContext(); CGContextSetTextMatrix(ctx, CGAffineTransformIdentity); CGContextConcatCTM(ctx, CGAffineTransformMake(1, 0, 0, -1, 0, self.bounds.size.height)); NSMutableAttributedString * content = [self buildAttributedString]; //創建CTFramesetterRef和CTFrameRef變量 CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)content); CGMutablePathRef paths = CGPathCreateMutable(); CGPathAddRect(paths, NULL, self.bounds); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, content.length), paths, NULL); CTFrameDraw(frame, ctx); //遍歷文本行以及CTRunRef,將表情文本對應的表情圖片繪制到圖形上下文 CFArrayRef lines = CTFrameGetLines(_frame); CGPoint lineOrigins[CFArrayGetCount(lines)]; CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), lineOrigins); for (int idx = 0; idx < CFArrayGetCount(lines); idx++) { CTLineRef line = CFArrayGetValueAtIndex(lines, idx); CGPoint lineOrigin = lineOrigins[idx]; CFArrayRef runs = CTLineGetGlyphRuns(line); //遍歷字符組 for (int index = 0; index < CFArrayGetCount(runs); index++) { CGFloat runAscent; CGFloat runDescent; CGPoint lineOrigin = lineOrigins[idx]; CTRunRef run = CFArrayGetValueAtIndex(runs, index); CGRect runRect; runRect.size.width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &runAscent, &runDescent, NULL); runRect = CGRectMake(lineOrigin.x + CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, NULL), lineOrigin.y - runDescent, runRect.size.width, runAscent + runDescent); //查找表情文本替換表情視圖 NSDictionary * attributes = (NSDictionary *)CTRunGetAttributes(run); NSString * imageName = attributes[@"imageNameKey"]; if (imageName) { UIImage * image = [UIImage imageNamed: imageName]; if (image) { CGRect imageDrawRect; CGFloat imageSize = ceil(runRect.size.height); imageDrawRect.size = CGSizeMake(imageSize, imageSize); imageDrawRect.origin.x = runRect.origin.x + lineOrigin.x; imageDrawRect.origin.y = lineOrigin.y; CGContextDrawImage(ctx, imageDrawRect, image.CGImage); } } } } CFRelease(paths); CFRelease(frame); CFRelease(framesetter); }
代碼運行之後,代碼的運行圖應該是這樣:
現在富文本像模像樣了,但我們怎樣才能在點擊超鏈接文本的時候發生響應回調呢?像圖片那樣判斷點擊是否處在rect范圍內的判斷是不可取的,因為超鏈接文本可能剛好處在換行的位置,從而存在多個rect。對此,CoreText同樣提供了一個函數CTLineGetStringIndexForPosition(CTLineRef, CGPoint)方法來獲取點擊坐標位於文本行的字符的下標位置。但在此之前,我們必須先獲取點擊點所在的文本行數位置,為了達到獲取文本行的目的,繪制文本的CTFrameRef變量必須保存下來,因此我定義了一個實例變量存儲文本渲染中生成的CTFrameRef。同樣的,對於超鏈接文本所在的位置,我們應該把這個位置轉換成字符串作為key值,文本對應的鏈接作為value值存到一個實例字典中。
@implementation LXDTextView { CTFrameRef _frame; NSMutableDictionary * _textTouchMapper; } - (NSMutableAttributedString *)buildattrinbutedstring { //do something... _textTouchMapper[NSStringFromRange(range)] = @"https://www.baidu.com"; //do something... } - (void)drawRect: (CGRect)rect { //do something... _frame = frame; //do something... } - (void)touchesEnded: (NSSet*)touches withEvent: (UIEvent *)event { CGPoint touchPoint = [touches.anyObject locationInView: self]; CFArrayRef lines = CTFrameGetLines(_frame); CGPoint origins[CFArrayGetCount(lines)]; for (int idx = 0; idx < CFArrayGetCount(lines); idx++) { CGPoint origin = origins[idx]; CGPathRef path = CTFrameGetPath(_frame); CGRect rect = CGPathGetBoundingBox(path); //將坐標點更改為左上角坐標系原點的坐標 CGFloat y = rect.origin.y + rect.size.height - origin.y; if (touchPoint.y = origin.x && touchPoint.x = range.location && index <= range.location + range.length) { NSLog(@"點擊了圖片鏈接:%@", _textTouchMapper[textRange]); break; } } } @end
現在運行代碼,看看點擊富文本的超鏈接時,控制台是不是輸出了點擊圖片鏈接了呢?
進一步封裝
我們已經完成了富文本的實現,下一步是思考如何從外界傳入文本內容和對應關系,然後顯示。因此,我們需要在頭文件中提供兩個字典類型的屬性,分別用於使用者傳入文本-超鏈接以及文本-表情圖片的對應關系:
@interface LXDTextView : UIView /*! * @brief 顯示文本(所有的鏈接文本、圖片名稱都應該放到這裡面) */ @property (nonatomic, copy) NSString * text; /*! * @brief 文本-超鏈接映射 */ @property (nonatomic, strong) NSDictionary * hyperlinkMapper; /*! * @brief 文本-表情映射 */ @property (nonatomic, strong) NSDictionary * emojiTextMapper; @end 當然,這時候buildAttributedString也應該進行相應的修改,由於富文本中可能存在多個表情,因此需要把往富文本中插入表情占位符的邏輯封裝出來。另一方面,把富文本對象content作為類成員變量來使用,會讓代碼更方便: /*! * @brief 在富文本中插入表情占位符,然後設置好屬性 * * @param imageName 表情圖片的名稱 * @param emojiRange 表情文本在富文本中的位置,用於替換富文本 */ - (void)insertEmojiAttributed: (NSString *)imageName emojiRange: (NSRange)emojiRange { CTRunDelegateCallbacks imageCallbacks; imageCallbacks.version = kCTRunDelegateVersion1; imageCallbacks.dealloc = RunDelegateDeallocCallback; imageCallbacks.getWidth = RunDelegateGetWidthCallback; imageCallbacks.getAscent = RunDelegateGetAscentCallback; imageCallbacks.getDescent = RunDelegateGetDescentCallback; /*! * @brief 插入圖片屬性文本 */ CTRunDelegateRef runDelegate = CTRunDelegateCreate(&imageCallbacks, (__bridge void *)imageName); NSMutableAttributedString * imageAttributedString = [[NSMutableAttributedString alloc] initWithString: @" "]; [imageAttributedString addAttribute: (NSString *)kCTRunDelegateAttributeName value: (__bridge id)runDelegate range: NSMakeRange(0, 1)]; [imageAttributedString addAttribute: LXDEmojiImageNameKey value: imageName range: NSMakeRange(0, 1)]; [_content deleteCharactersInRange: emojiRange]; [_content insertAttributedString: imageAttributedString atIndex: emojiRange.location]; CFRelease(runDelegate); }
在buildAttributedString方法也增加了根據傳入的兩個字典進行富文本字符替換的邏輯。其中表情文本替換應該放在超鏈接文本替換之前——因為表情文本最終替換成一個空格字符串,但是表情文本的長度往往總是大於1。先替換表情文本就不會導致超鏈接文本查找中的位置出錯:
- (void)buildAttributedString { _content = [[NSMutableAttributedString alloc] initWithString: _text attributes: self.textAttributes]; /*! * @brief 獲取所有轉換emoji表情的文本位置 */ for (NSString * emojiText in self.emojiTextMapper) { NSRange range = [_content.string rangeOfString: emojiText]; while (range.location != NSNotFound) { [self insertEmojiAttributed: self.emojiTextMapper[emojiText] emojiRange: range]; range = [_content.string rangeOfString: emojiText]; } } /*! * @brief 獲取所有轉換超鏈接的文本位置 */ for (NSString * hyperlinkText in self.hyperlinkMapper) { NSRange range = [_content.string rangeOfString: hyperlinkText]; while (range.location != NSNotFound) { [self.textTouchMapper setValue: self.hyperlinkMapper[hyperlinkText] forKey: NSStringFromRange(range)]; [_content addAttributes: @{ NSForegroundColorAttributeName: [UIColor blueColor] } range: range]; [_content addAttributes: @{ NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle) } range: range]; range = [_content.string rangeOfString: hyperlinkText]; } } }
當然了,如果你樂意的話,還可以順帶的增加一下文本換行類型等屬性的設置:
/*! * @brief 設置字體屬性 */ CTParagraphStyleSetting styleSetting; CTLineBreakMode lineBreak = kCTLineBreakByWordWrapping; styleSetting.spec = kCTParagraphStyleSpecifierLineBreakMode; styleSetting.value = &lineBreak; styleSetting.valueSize = sizeof(CTLineBreakMode); CTParagraphStyleSetting settings[] = { styleSetting }; CTParagraphStyleRef style = CTParagraphStyleCreate(settings, 1); NSMutableDictionary * attributes = @{ (id)kCTParagraphStyleAttributeName: (id)style }.mutableCopy; [_content addAttributes: attributes range: NSMakeRange(0, _content.length)]; CTFontRef font = CTFontCreateWithName((CFStringRef)[UIFont systemFontOfSize: 16].fontName, 16, NULL); [_content addAttributes: @{ (id)kCTFontAttributeName: (__bridge id)font } range: NSMakeRange(0, _content.length)]; CFRelease(font); CFRelease(style);
由於富文本的渲染流程不會因為富文本內容變化而變化,所以drawRect:內的邏輯幾乎沒有任何改變。但是同樣的,如果你需要擁有點擊表情的事件,那麼我們同樣需要像超鏈接文本實現的方式一樣增加一個用來判斷是否點擊在表情frame裡面的工具,將表情的rect作為key,表情圖片名字作為value,我把這個字典命名為emojiTouchMapper。此外,前文說過CoreText的坐標系跟常規坐標系是相反的,即使我們在drawRect:開頭翻轉了坐標系,在獲取這些文本坐標時,仍然是按照左下角坐標系計算的。因此如果不做適當處理,那麼在點擊的時候就沒辦法按照正常的frame來判斷是否處於點擊范圍內。因此我們需要在遍歷文本行之前聲明一個存儲文本內容最頂部的y坐標變量,在遍歷完成之後用這個變量依次和存儲的表情視圖進行坐標計算,從而存儲正確的frame
- (void)drawRect: (CGRect)rect { //do something... CGRect imageDrawRect; CGFloat imageSize = ceil(runRect.size.height); imageDrawRect.size = CGSizeMake(imageSize, imageSize); imageDrawRect.origin.x = runRect.origin.x + lineOrigin.x; imageDrawRect.origin.y = lineOrigin.y - lineDescent; CGContextDrawImage(ctx, imageDrawRect, image.CGImage); imageDrawRect.origin.y = topPoint - imageDrawRect.origin.y; self.emojiTouchMapper[NSStringFromCGRect(imageDrawRect)] = imageName; //do something... }
寫完上面的代碼之後,自定義的富文本視圖就已經完成了,最後需要實現的是點擊結束時判斷點擊位置是否處在表情視圖或者超鏈接文本上,然後進行相應的回調處理。這裡使用的是代理方式回調:
- (void)touchesEnded: (NSSet*)touches withEvent: (UIEvent *)event { CGPoint touchPoint = [touches.anyObject locationInView: self]; CFArrayRef lines = CTFrameGetLines(_frame); CGPoint origins[CFArrayGetCount(lines)]; CTFrameGetLineOrigins(_frame, CFRangeMake(0, 0), origins); CTLineRef line = NULL; CGPoint lineOrigin = CGPointZero; //查找點擊坐標所在的文本行 for (int idx = 0; idx < CFArrayGetCount(lines); idx++) { CGPoint origin = origins[idx]; CGPathRef path = CTFrameGetPath(_frame); CGRect rect = CGPathGetBoundingBox(path); //轉換點擊坐標 CGFloat y = rect.origin.y + rect.size.height - origin.y; if (touchPoint.y = origin.x && touchPoint.x = range.location && index <= range.location + range.length) { if ([_delegate respondsToSelector: @selector(textView:didSelectedHyperlink:)]) { [_delegate textView: self didSelectedHyperlink: self.textTouchMapper[textRange]]; } return; } } //判斷是否點擊表情 if (!_emojiUserInteractionEnabled) { return; } for (NSString * rectString in self.emojiTouchMapper) { CGRect textRect = CGRectFromString(rectString); if (CGRectContainsPoint(textRect, touchPoint)) { if ([_delegate respondsToSelector: @selector(textView:didSelectedEmoji:)]) { [_delegate textView: self didSelectedEmoji: self.emojiTouchMapper[rectString]]; } } } }
按照上面的代碼完成之後,我們實現了一個富文本控件,我用下面的代碼測試這個圖文混編:
LXDTextView * textView = [[LXDTextView alloc] initWithFrame: CGRectMake(0, 0, 200, 300)]; textView.delegate = self; [self.view addSubview: textView]; textView.emojiUserInteractionEnabled = YES; textView.center = self.view.center; textView.emojiTextMapper = @{ @"[emoji]": @"emoji" }; textView.hyperlinkMapper = @{ @"@百度": @"https://www.baidu.com", @"@騰訊": @"https://www.qq.com", @"@谷歌": @"https://www.google.com", @"@臉書": @"https://www.facebook.com", };
textView.text = @"很久很久以前[emoji],在一個群裡,生活著@百度、@騰訊這樣的居民,後來,一個[emoji]叫做@谷歌的人入侵了這個村莊,他的同伙@臉書讓整個群裡變得淫蕩無比。從此[emoji],迎來了污妖王的時代。污妖王,我當定了![emoji]";
運行效果:
CoreText
關於TextKit
使用CoreText來實現圖文混編十分的強大,但同樣帶來了更多的代碼,更復雜的邏輯。在iOS7之後蘋果推出了TextKit框架,基於CoreText進行的高級封裝無疑帶來了更簡潔的代碼,其中新的NSTextAttachment類能讓我們輕松的將圖片轉換成富文本,我們只需要下面這麼幾句代碼就能輕松的創建一個表情富文本:
- (NSAttributedString *)attributedStringWithImageName: (NSString *)imageName { NSTextAttachment * attachment = [[NSTextAttachment alloc] init]; attachment.image = [UIImage imageNamed: imageName]; attachment.bounds = CGRectMake(0, -5, 20, 20); NSAttributedString * attributed = [NSAttributedString attributedStringWithAttachment: attachment]; return attributed; }
上面CoreText中的那段富文本代碼就能改成下面這樣:
- (NSAttributedString *)attributedString: (NSMutableAttributedString *)attributedString replacingEmojiText: (NSString *)emojiText withImageName: (NSString *)imageName { NSRange range = [attributedString.string rangeOfString: emojiText]; while (range.location != NSNotFound) { [attributedString deleteCharactersInRange: range]; NSAttributedString * imageAttributed = [self attributedStringWithImageName: imageName]; [attributedString insertAttributedString: imageAttributed atIndex: range.location]; range = [attributedString.string rangeOfString: emojiText]; } return attributedString; } - (void)viewDidLoad { [super viewDidLoad]; NSAttributedString * attributed = [self attributedString: [[NSMutableAttributedString alloc] initWithString: content] replacingEmojiText: @"[emoji]" withImageName: @"emoji"]; UILabel * label = [[UILabel alloc] initWithFrame: CGRectMake(0, 0, 200, 250)]; label.attributedText = attributed; label.center = self.view.center; label.numberOfLines = 0; [self.view addSubview: label]; }
關心TextKit更多使用方式可以閱讀這篇文章:認識TextKit。
結尾
作為實現圖文混編的兩個框架,哪個功能更加強大,就只能見仁見智了。高級封裝的TextKit帶來了簡潔性,CoreText則是更靈活。不過截至文章寫完為止,我還沒有找到通過TextKit實現超鏈接文本點擊響應的功能。而對於開發者而言,這兩個框架都應該都去了解,才能更好的開發。
本文為投稿文章,作者:Sindri的小巢