TextKit

以前,如果我們想實(shí)現(xiàn)如上圖所示復(fù)雜的文本排版:顯示不同樣式的文本咱圆、圖片和文字混排循狰,你可能就需要借助于UIWebView或者深入研究一下Core Text榛做。在iOS6中,UILabel蚕苇、UITextField哩掺、UITextView增加了一個(gè)NSAttributedString屬性,可以稍微解決一些排版問題涩笤,但是支持的力度還不夠〗劳蹋現(xiàn)在Text Kit完全改變了這種現(xiàn)狀。

1.NSAttributedString

下面的例子蹬碧,展示如何label中顯示屬性化字符串:

-(void)setAttributeStringLabel{
    NSString *str = @"bold舱禽,little color,hello";
    
    //NSMutableAttributedString的初始化
    NSDictionary *attrs = @{NSFontAttributeName:[UIFont preferredFontForTextStyle:UIFontTextStyleBody]};
    NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc]initWithString:str attributes:attrs];
    
    //NSMutableAttributedString增加屬性
    [attributedString addAttribute:NSFontAttributeName value:[UIFont boldSystemFontOfSize:36] range:[str rangeOfString:@"bold"]];
    
    [attributedString addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:[str rangeOfString:@"little color"]];
    
    [attributedString addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"Papyrus" size:36] range:NSMakeRange(18,5)];
    
    [attributedString addAttribute:NSFontAttributeName value:[UIFont boldSystemFontOfSize:18] range:[str rangeOfString:@"little"]];
    
    //NSMutableAttributedString移除屬性
    [attributedString removeAttribute:NSFontAttributeName range:[str rangeOfString:@"little"]];
    
    //NSMutableAttributedString設(shè)置屬性
    NSDictionary *attrs2 = @{NSStrokeWidthAttributeName:@-5,
                             NSStrokeColorAttributeName:[UIColor greenColor],
                             NSFontAttributeName:[UIFont systemFontOfSize:36],
                             NSUnderlineStyleAttributeName:@(NSUnderlineStyleSingle)};
    [attributedString setAttributes:attrs2 range:NSMakeRange(0, 4)];
    
    self.label.attributedText = attributedString;
}

運(yùn)行結(jié)果如下:

需要注意的是恩沽,你不能直接修改已有的AttributedString, 你需要把它c(diǎn)opy出來誊稚,修改后再進(jìn)行設(shè)置:

NSMutableAttributedString *labelText = [myLabel.attributedText mutableCopy]; 
[labelText setAttributes:...];
myLabel.attributedText = labelText;

2.Dynamic type:動(dòng)態(tài)字體

iOS7增加了一項(xiàng)用戶偏好設(shè)置:動(dòng)態(tài)字體,用戶可以通過顯示與亮度-文字大小設(shè)置面板來修改設(shè)備上所有字體的尺寸罗心。為了支持這個(gè)特性里伯,意味著不要用systemFontWithSize:,而要用新的字體選擇器preferredFontForTextStyle:。iOS提供了六種樣式:標(biāo)題渤闷,正文疾瓮,副標(biāo)題,腳注飒箭,標(biāo)題1狼电,標(biāo)題2蜒灰。例如:

_textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];

你可以接收用戶改變字體大小的通知:

[[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(preferredContentSizeChanged:) name:UIContentSizeCategoryDidChangeNotification
                                               object:nil];

-(void)preferredContentSizeChanged:(NSNotification *)notification{
    _textView.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
}

3.Exclusion paths:排除路徑

iOS 上的 NSTextContainer 提供了exclusionPaths,它允許開發(fā)者設(shè)置一個(gè) NSBezierPath 數(shù)組來指定不可填充文本的區(qū)域肩碟。如下圖:

IMG_0934.PNG

正如你所看到的卷员,所有的文本都放置在藍(lán)色橢圓外面。在 Text View 里面實(shí)現(xiàn)這個(gè)行為很簡單腾务,但是有個(gè)小麻煩:Bezier Path 的坐標(biāo)必須使用容器的坐標(biāo)系。以下是轉(zhuǎn)換方法,將它的 bounds(self.circleView.bounds)轉(zhuǎn)換到 Text View 的坐標(biāo)系統(tǒng):

- (void)updateExclusionPaths
{
    CGRect ovalFrame = [self.textView convertRect:self.circleView.bounds fromView:self.circleView];    
}

因?yàn)闆]有 inset削饵,文本會過于靠近視圖邊界岩瘦,所以 UITextView 會在離邊界還有幾個(gè)點(diǎn)的距離的地方插入它的文本容器。因此窿撬,要得到以容器坐標(biāo)表示的路徑启昧,必須從 origin 中減去這個(gè)插入點(diǎn)的坐標(biāo)。

ovalFrame.origin.x -= self.textView.textContainerInset.left;
ovalFrame.origin.y -= self.textView.textContainerInset.top;

在此之后劈伴,只需將 Bezier Path 設(shè)置給 Text Container 即可將對應(yīng)的區(qū)域排除掉密末。其它的過程對你來說是透明的,TextKit 會自動(dòng)處理跛璧。

self.textView.textContainer.exclusionPaths = @[[UIBezierPath bezierPathWithOvalInRect: ovalFrame]];

4.多容器布局

屏幕快照 2015-03-10 下午2.52.15.png

NSTextStorage:它是NSMutableAttributedString的子類严里,里面存的是要管理的文本。
NSLayoutManager:管理文本布局方式
NSTextContainer:表示文本要填充的區(qū)域

如上圖所示追城,它們的關(guān)系是 1 對 N 的關(guān)系刹碾。就是那樣:一個(gè) Text Storage 可以擁有多個(gè) Layout Manager,一個(gè) Layout Manager 也可以擁有多個(gè) Text Container座柱。這些多重性帶來了多容器布局的特性:

1)將多個(gè) Layout Manager 附加到同一個(gè) Text Storage 上迷帜,可以產(chǎn)生相同文本的多種視覺表現(xiàn),如果相應(yīng)的 Text View 可編輯色洞,那么在某個(gè) Text View 上做的所有修改都會馬上反映到所有 Text View 上戏锹。

    NSTextStorage *sharedTextStorage = self.originalTextView.textStorage;
    [sharedTextStorage replaceCharactersInRange:NSMakeRange(0, 0) withString:kstring];
    
    
    // 將一個(gè)新的 Layout Manager 附加到上面的 Text Storage 上
    NSLayoutManager *otherLayoutManager = [NSLayoutManager new];
    [sharedTextStorage addLayoutManager: otherLayoutManager];
    
    NSTextContainer *otherTextContainer = [NSTextContainer new];
    [otherLayoutManager addTextContainer: otherTextContainer];
    
    UITextView *otherTextView = [[UITextView alloc] initWithFrame:self.otherContainerView.bounds textContainer:otherTextContainer];
    otherTextView.backgroundColor = self.otherContainerView.backgroundColor;
    otherTextView.translatesAutoresizingMaskIntoConstraints = YES;
    otherTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    
    otherTextView.scrollEnabled = NO;
    
    [self.otherContainerView addSubview: otherTextView];
    self.otherTextView = otherTextView;

2)將多個(gè) Text Container 附加到同一個(gè) Layout Manager 上,這樣可以將一個(gè)文本分布到多個(gè)視圖展現(xiàn)出來火诸。下面的例子將展示這兩個(gè)特性:

// 將一個(gè)新的 Text Container 附加到同一個(gè) Layout Manager锦针,這樣可以將一個(gè)文本分布到多個(gè)視圖展現(xiàn)出來。
    NSTextContainer *thirdTextContainer = [NSTextContainer new];
    [otherLayoutManager addTextContainer: thirdTextContainer];
    
    UITextView *thirdTextView = [[UITextView alloc] initWithFrame:self.thirdContainerView.bounds textContainer:thirdTextContainer];
    thirdTextView.backgroundColor = self.thirdContainerView.backgroundColor;
    thirdTextView.translatesAutoresizingMaskIntoConstraints = YES;
    thirdTextView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
    
    [self.thirdContainerView addSubview: thirdTextView];
    self.thirdTextView = thirdTextView;

結(jié)果如下所示:

IMG_0935.PNG

5.語法高亮:繼承NSTextStorage

看看 TextKit 組件的責(zé)任劃分惭蹂,就很清楚語法高亮應(yīng)該由 Text Storage 實(shí)現(xiàn)伞插。不過NSTextStorage 不是一個(gè)普通的類,它是一個(gè)類簇盾碗,你可以把它理解為一個(gè)"半具體"子類媚污,因此要繼承它必須實(shí)現(xiàn)以下方法:

- string;
- attributesAtIndex:effectiveRange:
- replaceCharactersInRange:withString:
- setAttributes:range:

我們新建一個(gè)NSTextStorage的子類:SyntaxHighlightTextStorage

要實(shí)現(xiàn)以上4個(gè)方法,我們首先需要通過NSMutableAttributedString 實(shí)現(xiàn)一個(gè)后備存儲廷雅,- setAttributes:range:這個(gè)方法需要用beginEditing和endEditing包起來耗美,而且必須調(diào)用 edited:range:changeInLength:京髓,所以大部分的NSTextStorage的子類都長下面這個(gè)樣子:

@implementation SyntaxHighlightTextStorage
{
    NSMutableAttributedString *_backingStore;
}

- (instancetype)init
{
    self = [super init];
    if (self) {
        _backingStore = [NSMutableAttributedString new];
    }
    return self;
}
//1
- (NSString *)string {
    return [_backingStore string];
}
//2
- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range
{
    return [_backingStore attributesAtIndex:location
                             effectiveRange:range];
}
//3
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
{
    NSLog(@"replaceCharactersInRange:%@ withString:%@",NSStringFromRange(range), str);
    [self beginEditing];
    [_backingStore replaceCharactersInRange:range withString:str];
    [self edited:NSTextStorageEditedCharacters | NSTextStorageEditedAttributes range:range changeInLength:str.length - range.length];
    [self endEditing];
}
//4
- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range {
    NSLog(@"setAttributes:%@ range:%@", attrs, NSStringFromRange(range));
    [self beginEditing];
    [_backingStore setAttributes:attrs range:range];
    [self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
    [self endEditing];
}

一個(gè)方便實(shí)現(xiàn)高亮的辦法是覆蓋 -processEditing,并設(shè)置一個(gè)正則表達(dá)式來查找單詞,每次文本存儲有修改時(shí),這個(gè)方法都自動(dòng)被調(diào)用商架。

- (void)processEditing
{
    [super processEditing];
    static NSRegularExpression *expression;
    expression = expression ?: [NSRegularExpression regularExpressionWithPattern:@"(\\*\\w+(\\s\\w+)*\\*)\\s" options:0 error:NULL];   
}

首先清除之前所有的高亮:

NSRange paragaphRange = [self.string paragraphRangeForRange: self.editedRange];
    [self removeAttribute:NSForegroundColorAttributeName range:paragaphRange];

其次遍歷所有的樣式匹配項(xiàng)并高亮它們:

[expression enumerateMatchesInString:self.string options:0 range:paragaphRange usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
        [self addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor] range:result.range];
    }];

就這樣堰怨,我們在文本系統(tǒng)棧里面有了一個(gè) Text Storage 的全功能替換版本。在從 Interface 文件中載入時(shí)蛇摸,可以像這樣將它插入文本視圖:

- (void)createTextView {
    _textStorage = [SyntaxHighlightTextStorage new];
    [_textStorage addLayoutManager: self.textView.layoutManager];
    
    [_textStorage replaceCharactersInRange:NSMakeRange(0, 0) withString:@"在從 Interface 文件中載入時(shí)备图,可以像這樣將它插入文本視圖,然后加 *星號* 的字就會高亮出來了"];
    _textView.delegate = self;
}

運(yùn)行如下:

IMG_0936.PNG

6.文本容器修改:繼承NSTextContainer

通過繼承NSTextContainer,我們可以使得textView不再是一個(gè)規(guī)規(guī)矩矩的矩形赶袄。NSTextContainer負(fù)責(zé)回答這個(gè)問題:對于給定的矩形揽涮,哪個(gè)部分可以放文字,這個(gè)問題由下面這個(gè)方法來回答:

- (CGRect)lineFragmentRectForProposedRect: atIndex: writingDirection: remainingRect:

所以我們在繼承NSTextContainer的類中覆蓋這個(gè)方法即可:

下面這個(gè)方法返回一個(gè)圓形區(qū)域:

- (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect
                                  atIndex:(NSUInteger)characterIndex
                         writingDirection:(NSWritingDirection)baseWritingDirection
                            remainingRect:(CGRect *)remainingRect {

  CGRect rect = [super lineFragmentRectForProposedRect:proposedRect
                                               atIndex:characterIndex
                                      writingDirection:baseWritingDirection
                                         remainingRect:remainingRect];

  CGSize size = [self size];
  CGFloat radius = fmin(size.width, size.height) / 2.0;
  CGFloat ypos = fabs((proposedRect.origin.y + proposedRect.size.height / 2.0) - radius);
  CGFloat width = (ypos < radius) ? 2.0 * sqrt(radius * radius - ypos * ypos) : 0.0;
  CGRect circleRect = CGRectMake(radius - width / 2.0, proposedRect.origin.y, width, proposedRect.size.height);

  return CGRectIntersection(rect, circleRect);
}

使用這個(gè)繼承類:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSString *path = [[NSBundle mainBundle] pathForResource:@"sample.txt" ofType:nil];
    NSString *string = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    
    NSMutableParagraphStyle *style = [NSMutableParagraphStyle new];
    [style setAlignment:NSTextAlignmentJustified];
    
    NSTextStorage *text = [[NSTextStorage alloc] initWithString:string
                                                     attributes:@{
                                                                  NSParagraphStyleAttributeName: style,
                                                                  NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleCaption2]
                                                                  }];
    NSLayoutManager *layoutManager = [NSLayoutManager new];
    [text addLayoutManager:layoutManager];
    
    CGRect textViewFrame = CGRectMake(20, 20, 280, 280);
    CircleTextContainer *textContainer = [[CircleTextContainer alloc] initWithSize:textViewFrame.size];
    [textContainer setExclusionPaths:@[ [UIBezierPath bezierPathWithOvalInRect:CGRectMake(80, 120, 50, 50)]]];
    
    [layoutManager addTextContainer:textContainer];
    
    UITextView *textView = [[UITextView alloc] initWithFrame:textViewFrame
                                               textContainer:textContainer];
    textView.allowsEditingTextAttributes = YES;
    textView.scrollEnabled = NO;
    textView.editable = NO;
    
    [self.view addSubview:textView];
}

效果如下:

IMG_0937.PNG

7.布局修改:繼承NSLayoutManager

利用NSLayoutManager的代理方法饿肺,我們可以輕松的設(shè)置行高:

- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager
  lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex
  withProposedLineFragmentRect:(CGRect)rect
{
    return floorf(glyphIndex / 100);
}

假設(shè)你的文本中有鏈接蒋困,你不希望這些鏈接被斷行分割。如果可能的話敬辣,一個(gè) URL 應(yīng)該始終顯示為一個(gè)整體雪标,一個(gè)單一的文本片段。沒有什么比這更簡單的了溉跃。

首先村刨,就像前面討論過的那樣,我們使用自定義的 Text Storage喊积,如下:

static NSDataDetector *linkDetector;
linkDetector = linkDetector ?: [[NSDataDetector alloc] initWithTypes:NSTextCheckingTypeLink error:NULL];

NSRange paragaphRange = [self.string paragraphRangeForRange: NSMakeRange(range.location, str.length)];
[self removeAttribute:NSLinkAttributeName range:paragaphRange];

[linkDetector enumerateMatchesInString:self.string
                               options:0
                                 range:paragaphRange
                            usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop)
{
    [self addAttribute:NSLinkAttributeName value:result.URL range:result.range];
}];

改變斷行行為就只需要實(shí)現(xiàn)一個(gè) Layout Manager 的代理方法:

- (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex
{
    NSRange range;
    NSURL *linkURL = [layoutManager.textStorage attribute:NSLinkAttributeName
                                                  atIndex:charIndex
                                           effectiveRange:&range];

    return !(linkURL && charIndex > range.location && charIndex <= NSMaxRange(range));   

結(jié)果就像下面這樣:

IMG_0938.PNG

你可以在這里下載完整的代碼烹困。如果你覺得對你有幫助,希望你不吝嗇你的star:)

參考:初識 TextKit,iOS 7 by Tutorials,iOS 7 Programming Pushing the Limits

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末乾吻,一起剝皮案震驚了整個(gè)濱河市髓梅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绎签,老刑警劉巖枯饿,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異诡必,居然都是意外死亡奢方,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門爸舒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蟋字,“玉大人,你說我怎么就攤上這事扭勉∪到保” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵涂炎,是天一觀的道長忠聚。 經(jīng)常有香客問我设哗,道長,這世上最難降的妖魔是什么两蟀? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任网梢,我火速辦了婚禮,結(jié)果婚禮上赂毯,老公的妹妹穿的比我還像新娘战虏。我一直安慰自己,他們只是感情好党涕,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布活烙。 她就那樣靜靜地躺著,像睡著了一般遣鼓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上重贺,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天骑祟,我揣著相機(jī)與錄音,去河邊找鬼气笙。 笑死次企,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的潜圃。 我是一名探鬼主播缸棵,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼谭期!你這毒婦竟也來了堵第?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤隧出,失蹤者是張志新(化名)和其女友劉穎踏志,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體胀瞪,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡针余,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了凄诞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片圆雁。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖帆谍,靈堂內(nèi)的尸體忽然破棺而出伪朽,到底是詐尸還是另有隱情,我是刑警寧澤既忆,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布驱负,位于F島的核電站嗦玖,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏跃脊。R本人自食惡果不足惜宇挫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望酪术。 院中可真熱鬧器瘪,春花似錦、人聲如沸绘雁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽庐舟。三九已至欣除,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間挪略,已是汗流浹背历帚。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留杠娱,地道東北人挽牢。 一個(gè)月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像摊求,于是被迫代替她去往敵國和親禽拔。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 卷首語 歡迎來到 objc.io 第五期室叉! 我們希望你跟我們一樣為 iOS 7 的發(fā)布而感到興奮睹栖。選擇這個(gè)做為本期...
    評評分分閱讀 558評論 0 4
  • iOS 7 引入了一個(gè)非常有用的新功能TextKit,使開發(fā)者可以通過方便的接口去修改文字的樣式和排版茧痕,而不需要直...
    星___塵閱讀 7,601評論 4 75
  • 0.TextKit包含類講解 如圖TextKit_1可以看到,我們一般能接觸到的文字控件全是由TextKit封裝而...
    破弓閱讀 1,766評論 0 10
  • 轉(zhuǎn)載: 經(jīng)典必看總結(jié) http://www.itnose.net/detail/6177538.html Text...
    F麥子閱讀 464評論 0 1
  • 轉(zhuǎn)載:https://yq.aliyun.com/articles/60173 https://objccn.io...
    F麥子閱讀 2,871評論 0 2