TextKit

以前,如果我們想實現(xiàn)如上圖所示復(fù)雜的文本排版:顯示不同樣式的文本、圖片和文字混排,你可能就需要借助于UIWebView或者深入研究一下Core Text。在iOS6中,UILabel、UITextField、UITextView增加了一個NSAttributedString屬性,可以稍微解決一些排版問題,但是支持的力度還不夠?,F(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;
}

運行結(jié)果如下:

需要注意的是,你不能直接修改已有的AttributedString, 你需要把它copy出來,修改后再進(jìn)行設(shè)置:

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

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

iOS7增加了一項用戶偏好設(shè)置:動態(tài)字體,用戶可以通過顯示與亮度-文字大小設(shè)置面板來修改設(shè)備上所有字體的尺寸。為了支持這個特性,意味著不要用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è)置一個 NSBezierPath 數(shù)組來指定不可填充文本的區(qū)域。如下圖:

IMG_0934.PNG

正如你所看到的,所有的文本都放置在藍(lán)色橢圓外面。在 Text View 里面實現(xiàn)這個行為很簡單,但是有個小麻煩: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];    
}

因為沒有 inset,文本會過于靠近視圖邊界,所以 UITextView 會在離邊界還有幾個點的距離的地方插入它的文本容器。因此,要得到以容器坐標(biāo)表示的路徑,必須從 origin 中減去這個插入點的坐標(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 會自動處理。

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)系。就是那樣:一個 Text Storage 可以擁有多個 Layout Manager,一個 Layout Manager 也可以擁有多個 Text Container。這些多重性帶來了多容器布局的特性:

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

    NSTextStorage *sharedTextStorage = self.originalTextView.textStorage;
    [sharedTextStorage replaceCharactersInRange:NSMakeRange(0, 0) withString:kstring];
    
    
    // 將一個新的 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)將多個 Text Container 附加到同一個 Layout Manager 上,這樣可以將一個文本分布到多個視圖展現(xiàn)出來。下面的例子將展示這兩個特性:

// 將一個新的 Text Container 附加到同一個 Layout Manager,這樣可以將一個文本分布到多個視圖展現(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 實現(xiàn)。不過NSTextStorage 不是一個普通的類,它是一個類簇,你可以把它理解為一個"半具體"子類,因此要繼承它必須實現(xiàn)以下方法:

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

我們新建一個NSTextStorage的子類:SyntaxHighlightTextStorage

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

@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];
}

一個方便實現(xiàn)高亮的辦法是覆蓋 -processEditing,并設(shè)置一個正則表達(dá)式來查找單詞,每次文本存儲有修改時,這個方法都自動被調(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];

其次遍歷所有的樣式匹配項并高亮它們:

[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)棧里面有了一個 Text Storage 的全功能替換版本。在從 Interface 文件中載入時,可以像這樣將它插入文本視圖:

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

運行如下:

IMG_0936.PNG

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

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

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

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

下面這個方法返回一個圓形區(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);
}

使用這個繼承類:

- (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è)你的文本中有鏈接,你不希望這些鏈接被斷行分割。如果可能的話,一個 URL 應(yīng)該始終顯示為一個整體,一個單一的文本片段。沒有什么比這更簡單的了。

首先,就像前面討論過的那樣,我們使用自定義的 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];
}];

改變斷行行為就只需要實現(xiàn)一個 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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容