iOS 文本相关-TextKit`

TextKit,发布于iOS 7 。由许多新的用来处理文本的 UIKit 类组成,

iOS7 之前的所有版本,(几乎)所有的文本都是 WebKit 来处理的。所有 UILabel、UITextField,以及 UITextView 都在后台以某种方式使用 web views 来进行文本布局和渲染。为了新的界面风格,它们全都被重新设计以使用 TextKit。

TextKit结构

image.png

字符串(String):

要绘制文本,那么必然有字符串来存储这段文本。在默认的结构中,NSTextStorage保存并管理这个字符串,在这种情况中,字符串可以远离绘制。
使用一个自定义的 NSTextStorage 就可以让文本在稍后动态地添加字体或颜色高亮等文本属性装饰。可以直接为文本组件使用自己的模型。要想实现这个功能,我们需要一个特别设计的 NSTextStorage

NSTextStorage

如果你把文本系统看做一个模型-视图-控制器(MVC)架构,这个类代表的是模型。
NSTextStorage 是一个中枢,它管理所有的文本和属性信息。系统只提供了两个存取器方法存取它们,并另外提供了两个方法来分别修改文本和属性。 NSTextStorage 是从它的父类 NSAttributedString 继承了这些方法。NSTextStorage:从文本系统看来仅仅是一个带有属性的字符串,附带一些扩展。这两者唯一的重大不同点是 NSTextStorage 包含了一个方法,可以把所有对其内容进行的修改以通知的形式发送出来。

属性和方法
// NSLayoutManager数组
@property (readonly, copy, NS_NONATOMIC_IOSONLY) NSArray *layoutManagers; 

// 添加布局管理器
- (void)addLayoutManager:(NSLayoutManager *)aLayoutManager;

// 移除布局管理器
- (void)removeLayoutManager:(NSLayoutManager *)aLayoutManager;

typedef NS_OPTIONS(NSUInteger, NSTextStorageEditActions) {
// 属性被添加、删除或更改。
    NSTextStorageEditedAttributes = (1 << 0),
// 字符被添加、删除或替换。
    NSTextStorageEditedCharacters = (1 << 1)
} API_AVAILABLE(macos(10.11), ios(7.0));
// 描述文本存储对象挂起的编辑类型的掩码。
@property (readonly, NS_NONATOMIC_IOSONLY) NSTextStorageEditActions editedMask;

// 更改的文本范围。
@property (readonly, NS_NONATOMIC_IOSONLY) NSRange editedRange;

/*
当前范围的长度与编辑前的长度之差。
此反映了已编辑范围的当前长度与其在开始编辑之前(即,第一次调用beginEditing方法或一次调用edited:range:changeInLength:方法之前的长度)之间的差。 每次调用edited:range:changeInLength:方法都会累积这种差异,直到最终处理更改。
*/
@property (readonly, NS_NONATOMIC_IOSONLY) NSInteger changeInLength;

/*
跟踪对文本存储对象所做的更改,从而允许文本存储记录更改的全部范围。

如果没有调用beginEditing,则此方法将调用processEditing。每次对属性字符串更改,NSTextStorage都会自动调用此方法。
子类重写或添加直接更改其属性字符串的方法时,应在更改后调用此方法。其他时候不应调用此方法。

editedMask
指定更改性质的掩码。
editedRange
更改之前受影响的字符范围。
delta
添加到oldRange或从其中删除的字符数。如果如掩码所示没有编辑任何字符,则其值无关紧要且未定义
*/
- (void)edited:(NSTextStorageEditActions)editedMask range:(NSRange)editedRange changeInLength:(NSInteger)delta;

/*
清除对文本存储对象的更改,并将发出通知以及调用delegate和layout manager的方法。
*/
- (void)processEditing;
// 一个布尔值,指示文本存储对象是否延迟固定属性。
@property (readonly, NS_NONATOMIC_IOSONLY) BOOL fixesAttributesLazily;

/*
使指定范围内的属性无效。
*/
- (void)invalidateAttributesInRange:(NSRange)range;

/*
确保属性修复在指定范围内发生。

在访问范围内的任何属性之前,使用惰性属性修复的NSTextStorage对象需要调用此方法。 如果需要,此方法使属性修复有机会发生。 
支持惰性的NSTextStorage子类必须从它们实现的所有属性访问器中调用此方法。
*/
- (void)ensureAttributesAreFixedInRange:(NSRange)range;


@protocol NSTextStorageDelegate 
@optional

/*
当文本存储对象将要处理编辑时发送。
*/
- (void)textStorage:(NSTextStorage *)textStorage willProcessEditing:(NSTextStorageEditActions)editedMask range:(NSRange)editedRange changeInLength:(NSInteger)delta API_AVAILABLE(macos(10.11), ios(7.0));

// 当文本存储对象完成处理编辑时发送。
- (void)textStorage:(NSTextStorage *)textStorage didProcessEditing:(NSTextStorageEditActions)editedMask range:(NSRange)editedRange changeInLength:(NSInteger)delta API_AVAILABLE(macos(10.11), ios(7.0));

@end

UITextView

堆栈的另一头是实际的视图。在 TextKit 中,有两个目的:
第一,它是文本系统用来绘制的视图。文本视图它自己并不会做任何绘制;它仅仅提供一个供其它类绘制的区域。作为视图层级机构中唯一的组件,
第二个目的是处理所有的用户交互。具体来说,Text View 实现 UITextInput 的协议来处理键盘事件,它为用户提供了一种途径来设置一个插入点或选择文本。它并不对文本做任何实际上的改变,仅仅将这些改变请求转发给刚刚讨论的 Text Storage。

NSTextContainer

每个 Text View 定义了一个文本可以绘制的区域。为此,每个 Text View 都有一个 Text Container,它精确地描述了这个可用的区域。
在简单的情况下,这是一个垂直的无限大的矩形区域。文本被填充到这个区域,并且 Text View 允许用户滚动它。然而,在更高级的情况下,这个区域可能是一个特定大小的矩形。
例如,当渲染一本书时,每一页都有最大的高度和宽度。 Text Container 会定义这个大小,并且不接受任何超出的文本。相同情况下,一幅图像可能占据了页面的一部分,文本应该沿着它的边缘重新排版。这也是由 Text Container 来处理的。

属性和方法
// 文本容器的布局管理器。
@property  NSLayoutManager *layoutManager;
// 替换布局管理器。
- (void)replaceLayoutManager:(NSLayoutManager *)newLayoutManager
/*
文本容器的边框的大小。
从lineFragmentRectForProposedRect:atIndex:writingDirection:remainingRect:返回的布局区域的最大大小。 值小于等于0.0表示没有限制。
此属性的默认值为CGSizeZero。
*/
@property  CGSize size;

// 路径对象数组,代表文本在文本容器中不显示的区域。
@property  NSArray *exclusionPaths;

// 文本容器内最后一行的行为。
@property NSLineBreakMode lineBreakMode

/*
线段矩形内的文本插入值。

填充出现在行片段矩形的开头和结尾。 NSLayoutManager使用此值来确定布局宽度。 此属性的默认值为5.0。
行片段填充不用于表示文本边距。 
*/ 
@property CGFloat lineFragmentPadding;

// 文本容器可以存储的最大行数。
@property  NSUInteger maximumNumberOfLines

/*
返回文本容器内建议矩形的线段矩形的边界。

线段矩形的边界由proposalRect和其NSTextContainer属性定义的文本容器的边界矩形的交点确定。 NSTextContainer属性定义的区域将从返回值中排除。由于排除路径的原因,proposedRect可能会分为多个行片段。在这种情况下,remainingRect返回可以作为下一个迭代的建议矩形传递的余数。
子类可以覆盖此方法,以进行进一步的文本容器区域自定义。

proposalRect
用于放置布局管理器建议的文本的矩形。
characterIndex
文本存储中正在处理的行片段的字符位置。
baseWritingDirection
可视水平线内线段的前进方向。传递给该方法的值为NSWritingDirectionLeftToRight或NSWritingDirectionRightToLeft。
remainingRect
建议的矩形的其余部分不包括在返回的矩形中。可以将其作为建议的矩形传递给下一个迭代。
*/
- (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect atIndex:(NSUInteger)characterIndex writingDirection:(NSWritingDirection)baseWritingDirection remainingRect:(nullable CGRect *)remainingRect

/*
一个布尔值,指示文本容器的区域是否是没有孔或间隙的矩形,并且其边缘与文本视图的坐标系轴平行。

如果接收方是简单地由-size定义的矩形,则返回YES。 TextKit利用此信息来启用各种布局优化。 当此属性为NO时,NSLayoutManager禁用非连续布局。 当-exclusionPaths具有1个或多个项目,-maximumNumberOfLines不为0或-lineFragmentRectForProposedRect:atIndex:writingDirection:remainingRect:被覆盖时,默认实现返回NO。 当-lineFragmentRectForProposedRect:atIndex:writingDirection:remainingRect:被覆盖时,建议覆盖此属性。
*/
@property  BOOL simpleRectangularTextContainer

/*
一个布尔值,控制文本容器在调整其文本视图的大小时是否调整其边界矩形的宽度/高度。
当此属性的值为YES时,文本容器的文本视图的宽度/高度更改时,文本容器将调整其宽度/高度。 此属性的默认值为NO。
*/
@property (NS_NONATOMIC_IOSONLY) BOOL widthTracksTextView;
@property (NS_NONATOMIC_IOSONLY) BOOL heightTracksTextView;

NSLayoutManager

Layout Manager 是中心组件,它把所有组件粘合在一起:

  1. 这个管理器监听 Text Storage 中文本或属性改变的通知,一旦接收到通知就触发布局进程。
  2. 从 Text Storage 提供的文本开始,它将所有的字符翻译为字形(Glyph)。
  3. 一旦字形全部生成,这个管理器向它的 Text Containers 查询文本可用以绘制的区域。
    然后这些区域被行逐步填充,而行又被字形逐步填充。一旦一行填充完毕,下一行开始填充。
  4. 对于每一行,布局管理器必须考虑断行行为(放不下的单词必须移到下一行)、连字符、内联的图像附件等等。
  5. 当布局完成,文本的当前显示状态被设为无效,然后 Layout Manager 将前面几步排版好的文本设给 Text View。
属性和方法
/*
包含要布局内容的文本存储对象。

通过-[NSTextStorage addLayoutManager:]将布局管理器添加到文本存储中,
使用该属性来分配新的文本存储。
*/ 
@property  NSTextStorage *textStorage;
/*
布局管理器的当前文本容器。
*/
@property  NSArray *textContainers;

/*
添加 textContainer

将一个容器添加到数组的末尾。 必须使前一个最后一个容器之后的所有字形的布局无效(即,以前未布局的字形,因为它们无法容纳在任何地方)。
*/
- (void)addTextContainer:(NSTextContainer *)container;

/*
插入文本容器。

将容器插入数组中位于索引处的容器之前。 必须使容器中所有字形的布局无效
*/
- (void)insertTextContainer:(NSTextContainer *)container atIndex:(NSUInteger)index;

/*
移除文本容器。

从数组中删除索引处的容器。 必须使要删除的容器及其后的所有容器中的所有字形的布局无效。
*/
- (void)removeTextContainerAtIndex:(NSUInteger)index;

/*
使指定的文本容器和所有后续文本容器对象的布局信息(可能还有字形)无效。

文本系统的其他组件会自动调用此方法。 您几乎不需要直接调用它。 但是,NSTextContainer的子类在形状大小改变时必须调用此方法(例如,动态调整其形状以将文本环绕在放置的图形上的文本容器,例如,在添加,移动或删除图形时必须这样做)。 。
*/
- (void)textContainerChangedGeometry:(NSTextContainer *)container;

// 一个布尔值,指示是否用可见字形替换空白和其他通常不可见的字符。
// 如果是,则空白和其他“不可见”字符将以特殊字形或其他图形显示。 默认为“否”。
@property  BOOL showsInvisibleCharacters;

// 一个布尔值,指示布局管理器是否用可见字形代替布局中的控制字符。
// 如果为“是”,则将显式显示控制字符(通常类似于“ ^ M”)。 默认为“否”。
@property BOOL showsControlCharacters;

// 一个布尔值,指示布局管理器是否使用字体的开头。
@property  BOOL usesFontLeading;

/*
一个布尔值,指示布局管理器是否允许不连续的布局。

如果是,则布局管理器可以为文本的给定部分执行字形生成和布局,而无需前面部分的字形或布局。 默认为“否”。 
启用此设置将显着更改当调用给定的生成原因方法时,文本的哪些部分将执行字形生成或布局。 
它还具有显着的性能优势,尤其是对于大型文档。
*/ 
@property BOOL allowsNonContiguousLayout

/*
一个布尔值,指示布局管理器当前是否具有不连续布局的任何区域。
 如果当前可能存在布局的文本的不连续部分,则此方法返回YES。
*/
@property  BOOL hasNonContiguousLayout

/*
一个布尔值,指示布局管理器是否中止布局以用于异常长或可疑的输入。
如果为是,则对恶意输入启用内部安全性分析并激活防御行为。 通过启用此功能,某些文本(例如很长的段落)可能会导致意外的布局。 默认情况下为“否”。
*/
@property BOOL limitsLayoutForSuspiciousContents

/*
一个布尔值,指示布局管理器是否使用默认的连字符规则来换行。'

当该属性的值为YES时,布局管理器将尽最大努力尝试在换行时为文本加连字符。 您可以使用NSParagraphStyle的hyphenationFactor属性逐段覆盖此断字行为。此属性的默认值为NO,这可以防止布局管理器断字。
*/
@property BOOL usesDefaultHyphenation

/*
在指定的字符范围内使字形无效并进行调整。

此方法使给定字符范围内的字符的缓存字形无效,并通过长度变化来调整所有后续字形的字符索引,并使新字符范围无效。此方法仅使字形信息无效,并且不执行字形生成或布局。
因为使字形无效也使布局无效,所以在调用此方法之后,还应该调用invalidateLayoutForCharacterRange:actualCharacterRange:,并将charRange作为第一个参数传递。
此方法由布局机制使用,并且仅在排版期间才应调用,几乎在所有情况下都仅由排版员调用。
*/
- (void)invalidateGlyphsForCharacterRange:(NSRange)charRange changeInLength:(NSInteger)delta actualCharacterRange:(nullable NSRangePointer)actualCharRange;

/*
使映射到指定字符范围的字形的布局信息无效

此方法与invalidateLayoutForCharacterRange:isSoft:actualCharacterRange:的标记设置为NO具有相同的效果。
此方法只会使信息无效; 它不执行任何字形生成或布局。 您几乎不需要调用此方法。

charRange
要无效的字符范围。
ActualCharRange
如果不为NULL,则在输出时,在进行任何必要的扩展后,实际范围将无效。
*/
- (void)invalidateLayoutForCharacterRange:(NSRange)charRange actualCharacterRange:(nullable NSRangePointer)actualCharRange;

/*
使显示的字形或字符范围无效。 对于字符范围,该范围的未放置部分会被记住,并在可用布局后的某个时候重新显示。 对于字形范围变体,将忽略范围中尚未生成字形的任何部分。 两种方法均未真正导致布局。
*/
/*
使指定字符范围的显示无效。
布局可用时,区域中未布局的部分会被记住并重新显示。 实际上不会导致布局。
*/
- (void)invalidateDisplayForCharacterRange:(NSRange)charRange;
/*
使一系列字形无效,需要新的布局信息,并更新显示这些字形的任何文本视图的适当区域。
*/
- (void)invalidateDisplayForGlyphRange:(NSRange)glyphRange;

/*
当编辑操作更改其文本存储对象的内容时通知布局管理器。

NSTextStorage的processEditing方法调用此方法以将编辑操作通知布局管理器。布局管理器在执行此消息期间不得更改文本存储的内容。

textStorage
文本存储对象处理进行编辑。
editMask
完成的编辑类型:NSTextStorageEditedAttributes和/或NSTextStorageEditedCharacters。
newCharRange
明确编辑的最终字符串中的范围。
delta
编辑的长度变化量会改变。
invalidatedCharRange
由于属性修复而改变的字符范围。此无效范围等于newCharRange或更大。
*/
- (void)processEditingForTextStorage:(NSTextStorage *)textStorage edited:(NSTextStorageEditActions)editMask range:(NSRange)newCharRange changeInLength:(NSInteger)delta invalidatedRange:(NSRange)invalidatedCharRange

/*
这些方法使客户端可以准确地指定他们希望具有字形或布局的文档部分。 如果启用了非连续布局,这一点尤其重要。 布局管理器仍然保留对较大范围执行字形生成或布局的权利。 如果未启用非连续布局,则所讨论的范围将始终有效地扩展为从文本开头开始。
*/
// 强制布局管理器为指定字符范围生成字形(如果尚未生成)
- (void)ensureGlyphsForCharacterRange:(NSRange)charRange;
// 强制布局管理器为指定字形范围生成字形(如果尚未生成)。
- (void)ensureGlyphsForGlyphRange:(NSRange)glyphRange;
// 强制布局管理器为指定的字符范围执行布局(如果尚未完成)。
- (void)ensureLayoutForCharacterRange:(NSRange)charRange;
// 强制布局管理器为指定的字形范围执行布局(如果尚未完成)。
- (void)ensureLayoutForGlyphRange:(NSRange)glyphRange;
// 强制布局管理器为指定的文本容器执行布局(如果尚未进行的话)。
- (void)ensureLayoutForTextContainer:(NSTextContainer *)container;
// 强制布局管理器为指定文本容器中的指定区域执行布局(如果尚未进行的话)。
- (void)ensureLayoutForBoundingRect:(CGRect)bounds inTextContainer:(NSTextContainer *)container;
/*
存储字符范围的初始字形和字形属性。

在字形生成过程中,文本系统将调用此方法。允许应用程序直接调用此方法的唯一地方是来自NSLayoutManagerDelegate协议方法的实现,方法是layoutManager:shouldGenerateGlyphs:properties:characterIndexes:font:forGlyphRange:。
每个数组都有glyphRange.length项。指定的charIndexes必须是连续的(没有跳过的索引),以使多个项目具有相同的字符索引(例如当一个字符索引生成多个字形ID时)。由于字体替换,传递给此方法的aFont可能与属性字典中的字体不匹配。为先前已计算过布局信息的字符范围调用此方法会使布局和显示无效。

glyphs
指向布局管理器的字形缓存的指针。
props
指向缓冲区的指针,该缓冲区包含高速缓存中字形的字形属性。
charIndexes
指向文本存储器中字符的起始索引的指针,将为其生成字形。
aFont
用于覆盖文本存储中指定字符范围的字体属性的字体。
glyphRange
要设置的字形缓存中的字形范围。
*/ 
- (void)setGlyphs:(const CGGlyph *)glyphs properties:(const NSGlyphProperty *)props characterIndexes:(const NSUInteger *)charIndexes font:(UIFont *)aFont forGlyphRange:(NSRange)glyphRange

//布局管理器中字形的数量。
//如果未启用非连续布局,则将强制为所有字符生成字形。
@property  NSUInteger numberOfGlyphs;

/*
如果未启用非连续布局,则这些方法将导致生成所有字形,直到glyphIndex为止。 如果请求的索引超出范围(0,numberOfGlyphs),则第一个CGGlyphAtIndex变体返回kCGFontIndexInvalid,并可选地返回一个标志,指示请求的索引是否在范围内。 如果请求的索引超出范围,则第二个CGGlyphAtIndex变量将引发NSRangeError。
*/
// 返回指定索引处的字形以及有关字形索引是否有效的信息。

- (CGGlyph)CGGlyphAtIndex:(NSUInteger)glyphIndex isValidIndex:(nullable BOOL *)isValidIndex
// 返回指定索引处的字形。
- (CGGlyph)CGGlyphAtIndex:(NSUInteger)glyphIndex 
// 指示指定的索引是否引用有效的字形。
- (BOOL)isValidGlyphIndex:(NSUInteger)glyphIndex

typedef NS_OPTIONS(NSInteger, NSGlyphProperty) {
    NSGlyphPropertyNull = (1 << 0), // 空字形,布局管理器将忽略它。
    NSGlyphPropertyControlCharacter = (1 << 1), // 具有相关特殊行为的控制字符(例如选项卡,附件等)。
    NSGlyphPropertyElastic = (1 << 2), // 具有可变宽度的字形,例如空白字符。
    NSGlyphPropertyNonBaseCharacter = (1 << 3) // 这种类型的字形通常代表Unicode Mn类中的字符。
}
// 返回指定索引处字形的字形属性。
- (NSGlyphProperty)propertyForGlyphAtIndex:(NSUInteger)glyphIndex

// 返回文本存储中指定字形的第一个字符的索引。
//最好使用范围映射方法characterRangeForGlyphRange:actualGlyphRange:和glyphRangeForCharacterRange:actualCharacterRange:提供更全面
- (NSUInteger)characterIndexForGlyphAtIndex:(NSUInteger)glyphIndex;
// 返回指定索引处字符的第一个字形的索引。
//最好使用范围映射方法characterRangeForGlyphRange:actualGlyphRange:和glyphRangeForCharacterRange:actualCharacterRange:提供更全面
- (NSUInteger)glyphIndexForCharacterAtIndex:(NSUInteger)charIndex;

/*
用一系列字形填充传入的缓冲区。

glyphRange
填充的字形范围。
glyphBuffer
输出时,在给定字形范围内的字形序列。
props
如果不为NULL,则在输出时,对应于已填充字形的字形属性。
charIndexBuffer
如果不为NULL,则在输出时,对应于给定字形范围的原始字符的索引。请注意,索引1处的字形不必映射到索引1处的字符,因为字形可能用于连字或重音。
bidiLevelBuffer
如果不为NULL,则在输出时为双向文本的每个字形的方向。值范围从0到61,如Unicode标准附件#9所定义。偶数表示字形从左到右,而奇数值表示字形从右到左。
*/
- (NSUInteger)getGlyphsInRange:(NSRange)glyphRange glyphs:(nullable CGGlyph *)glyphBuffer properties:(nullable NSGlyphProperty *)props characterIndexes:(nullable NSUInteger *)charIndexBuffer bidiLevels:(nullable unsigned char *)bidiLevelBuffer

/*
将文本容器与指定字形范围相关联。

容器内的布局由setLineFragmentRect:forGlyphRange:usedRect:和setLocation:forStartOfGlyphRange:方法指定。
对于设置的每个字形范围,在设置行片段rect或任何布局位之前,排版人员应首先调用此方法。 此方法会将几个关键的布局属性(如未显示,并在线段外绘制)重置为其默认值。 在布局过程中,所有字形都应最终包含在传递给此方法的范围内。 传递的范围不应是该文本容器的整个字形范围; 通常,实际上,这就是给定线段在该容器中布局的范围。

aTextContainer
要设置的文本容器。
glyphRange
布局的字形范围。
*/
- (void)setTextContainer:(NSTextContainer *)container forGlyphRange:(NSRange)glyphRange;

/*
关联指定字形范围的线段边界。

排版人员必须首先使用setTextContainer:forGlyphRange:指定文本容器,然后再使用setLocation:forStartOfGlyphRange:设置字形的确切位置。
在设置行片段rect之后,设置位置或任何布局位之前,排版员应第二次调用此方法。 在布局过程中,所有字形都应最终包含在传递给此方法的范围内,但是只有以换行符开头的字形应位于此范围的开头。 线段矩形和线段使用的矩形始终位于容器坐标中。

fragmentRect
线段的矩形。
glyphRange
与fragmentRect相关联的字形范围。
usedRect
fragmentRect的一部分,实际上包含绘制的字形或其他标记(包括文本容器的行片段填充)。必须等于或包含在fragmentRect中。
*/
- (void)setLineFragmentRect:(CGRect)fragmentRect forGlyphRange:(NSRange)glyphRange usedRect:(CGRect)usedRect;

/*
设置额外的线段的边界和容器。 

当文本后缀以硬换行符结尾或文本后缀完全为空时,使用多余的行片段来定义需要在文本末尾显示的多余行。 线段矩形和线段使用的矩形始终位于容器坐标中。 仅当存在非空的额外行片段时,才应调用此方法。

fragmentRect
要设置的矩形。
usedRect
指示绘制插入点的位置。
aTextContainer
放置矩形的文本容器。
*/
- (void)setExtraLineFragmentRect:(CGRect)fragmentRect usedRect:(CGRect)usedRect textContainer:(NSTextContainer *)container;

/*
在指定范围内设置第一个字形的位置。
在设置字形范围的位置之前,必须使用setTextContainer:forGlyphRange:指定文本容器,并使用setLineFragmentRect:forGlyphRange:usedRect:指定线段矩形。
设置给定范围的第一个字形的位置。 设置字形范围的位置意味着它的第一个字形相对于前一个字形没有名义上的间隔。 在布局过程中,所有字形都应最终包含在传递给此方法的范围内,但是只有开始新的标称范围的字形才应位于此类范围的开头。 线段中的第一个字形应始终开始一个新的标称范围。 字形的位置是相对于其线段rect的原点给出的。

location
相对于字形线段原点的原点,设置第一个字形的位置。
glyphRange
位置已设置的字形。
*/
- (void)setLocation:(CGPoint)location forStartOfGlyphRange:(NSRange)glyphRange;

/*
设置字形在指定索引处的可见性。

排版员决定不显示哪些字形,并在布局管理器中设置此属性,以确保不显示这些字形。 例如,制表符或换行符不会留下任何标记; 它仅指示后续字形的布局位置。
如果glyphIndex超出范围,则引发NSRangeException。

flag
如果是,则不显示该字形。 如果否,则显示。
glyphIndex
设置了属性的字形的索引
*/
- (void)setNotShownAttribute:(BOOL)flag forGlyphAtIndex:(NSUInteger)glyphIndex;

/*
指示指定的字形是否超出其布局的线段范围。

用于指示特定字形由于某种原因会在其线段rect之外绘制。 如果使用固定的线高(通常考虑12点线高和24点字形),通常会发生这种情况。 此信息对于确定是否由于更改任何给定的线段而需要重新绘制其他线很重要。

flag
如果是,则设置给定的字形以绘制其线段之外; 如果为否,则字形不会向外绘制。
glyphIndex
要设置的字形的索引。
*/
- (void)setDrawsOutsideLineFragment:(BOOL)flag forGlyphAtIndex:(NSUInteger)glyphIndex;

/*
设置绘制表示附件的字形时要使用的大小。

对于与附件相对应的字形,应调用此方法来设置附件单元将占用的大小。 该字形的值应为NSControlGlyph。


attachmentSize
要设置的字形大小。
glyphRange
附件字形在字形流中的位置。
*/
- (void)setAttachmentSize:(CGSize)attachmentSize forGlyphRange:(NSRange)glyphRange;

/*
返回具有无效布局信息的第一个字符和字形的索引。
这两个参数都可以为NULL,在这种情况下,接收方只会忽略它。
作为其实现的一部分,此方法调用firstUnlaidCharacterIndex和firstUnlaidGlyphIndex。 要更改此方法的行为,请替代这两种方法。
*/ 
- (void)getFirstUnlaidCharacterIndex:(nullable NSUInteger *)charIndex glyphIndex:(nullable NSUInteger *)glyphIndex;
//返回具有无效布局信息的第一个字符的索引。
- (NSUInteger)firstUnlaidCharacterIndex;
返回具有无效布局信息的第一个字形的索引。
- (NSUInteger)firstUnlaidGlyphIndex;

/*
返回管理指定字形的布局的文本容器,从而使布局在必要时发生。并返回(可选)引用该容器中字形的整个范围

 这将导致包含指定字形的线段的字形生成和布局,或者如果未启用非连续布局,则直到该线段并包括该线段; 如果未启用非连续的布局,并且有效的GlyphRange为非NULL,则这还将另外导致包含指定字形的整个文本容器的字形生成和布局。

glyphIndex
返回容器中字形的索引。
effectiveGlyphRange
如果不为NULL,则在输出时指向返回的容器中的整个字形范围。
*/
- (nullable NSTextContainer *)textContainerForGlyphAtIndex:(NSUInteger)glyphIndex effectiveRange:(nullable NSRangePointer)effectiveGlyphRange;
/*
此方法主要在NSTypesetter内部使用,在完成有关范围的布局之后,但在布局管理器对NSTypesetter的调用返回之前。在那种情况下,字形和布局孔尚未被重新计算,因此布局管理器尚不知道该范围的布局是完整的,因此必须使用此变体。

glyphIndex
返回容器中字形的索引。
effectiveGlyphRange
如果不为NULL,则在输出时指向返回的容器中的整个字形范围。
flag
如果是,则不执行字形生成和布局,因此,除非已知该范围的布局是完整的,或者除非启用了非连续布局,否则不应使用此选项。如果否,则均根据需要执行。
*/
- (nullable NSTextContainer *)textContainerForGlyphAtIndex:(NSUInteger)glyphIndex effectiveRange:(nullable NSRangePointer)effectiveGlyphRange withoutAdditionalLayout:(BOOL)flag 

/*
返回指定文本容器中字形的边界矩形。

返回文本容器当前使用的区域,该区域确定视图所需的大小,以便显示容器中当前布置的所有字形。 这既不会产生字形也不会引起布局。
使用的矩形始终位于容器坐标中。
*/
- (CGRect)usedRectForTextContainer:(NSTextContainer *)container;

/*
返回字形所在的线段的矩形,以及(可选)返回该段中字形的整个范围。

这将导致包含指定字形的线段的字形生成和布局,或者如果未启用非连续布局,则直到该线段(包括该线段)。 线段矩形始终位于容器坐标中。
不建议覆盖此方法。 如果线段矩形需要修改,则应在排字机级别或通过调用setLineFragmentRect:forGlyphRange:usedRect:来完成。

glyphIndex
返回其线段矩形的字形。
effectiveGlyphRange
如果不为NULL,则在输出时为线段中所有字形的范围。
*/
- (CGRect)lineFragmentRectForGlyphAtIndex:(NSUInteger)glyphIndex effectiveRange:(nullable NSRangePointer)effectiveGlyphRange;
/*
此方法主要在NSTypesetter内部使用,在完成有关范围的布局之后,但在布局管理器对NSTypesetter的调用返回之前。在那种情况下,字形和布局孔尚未被重新计算,因此布局管理器尚不知道该范围的布局是完整的,因此必须使用此变体。
不建议覆盖此方法。如果线段矩形需要修改,则应在排字机级别或通过调用setLineFragmentRect:forGlyphRange:usedRect:来完成。

glyphIndex
返回其线段矩形的字形。
effectiveGlyphRange
如果不为NULL,则在输出时为线段中所有字形的范围。
flag
如果是,则不执行字形生成和布局,因此,除非已知该范围的布局是完整的,或者除非启用了非连续布局,否则不应使用此选项。如果否,则均根据需要执行。
*/
- (CGRect)lineFragmentRectForGlyphAtIndex:(NSUInteger)glyphIndex effectiveRange:(nullable NSRangePointer)effectiveGlyphRange withoutAdditionalLayout:(BOOL)flag

/*
返回该行片段的用法矩形,并(可选)返回该片段中字形的整个范围。

这将导致包含指定字形的线段的字形生成和布局,或者如果未启用非连续布局,则直到该线段(包括该线段)。 线段使用的矩形始终位于容器坐标中。
不建议覆盖此方法。 如果需要修改使用矩形的线段,则应在排字机级别或通过调用setLineFragmentRect:forGlyphRange:usedRect:来完成。

glyphIndex
要为其返回线段的字形使用矩形。
effectiveGlyphRange
如果不为NULL,则在输出时为线段中所有字形的范围。
*/
- (CGRect)lineFragmentUsedRectForGlyphAtIndex:(NSUInteger)glyphIndex effectiveRange:(nullable NSRangePointer)effectiveGlyphRange;
/*
flag
如果是,则不执行字形生成和布局,因此,除非已知该范围的布局是完整的,或者除非启用了非连续布局,否则不应使用此选项。如果否,则均根据需要执行。
*/
- (CGRect)lineFragmentUsedRectForGlyphAtIndex:(NSUInteger)glyphIndex effectiveRange:(nullable NSRangePointer)effectiveGlyphRange withoutAdditionalLayout:(BOOL)flag 

/*
返回有关多余行片段的信息。 
当文档中的最后一个字符导致行或段落中断时,多余的行片段用于在文档末尾显示行。 
由于多余的行没有与布局管理器中的任何字形关联,因此信息与其他行片段rect分开处理。 
通常,多余的行片段与其他常规行片段rect一起放置在最后一个文档内容textContainer中。 线段矩形和线段使用的矩形始终位于容器坐标中。
*/
//文档末尾的额外行片段的矩形。
@property  CGRect extraLineFragmentRect;
// 将插入点包围在额外的线段矩形中的矩形。
@property CGRect extraLineFragmentUsedRect;
// 额外的行片段矩形的文本容器。
@property  NSTextContainer *extraLineFragmentTextContainer;

/*
返回指定字形在其线段中的位置。

如果给定字形没有为其设置显式位置(例如,如果它是名义上间隔字符序列的一部分(但不是第一个)),则该位置是根据最近的字形的位置来计算的 具有位置集的前一个字形。
字形位置相对于其线段矩形的原点。 线段矩形又在其所在的文本容器的坐标系中定义。
此方法导致包含指定字形的线段的字形生成和布局,或者如果未启用非连续布局,则直到该线段(包括该线段)。
*/
- (CGPoint)locationForGlyphAtIndex:(NSUInteger)glyphIndex;

/*
指示指定索引处的字形是否具有可见的表示形式

一些字形未显示。 例如,未显示制表符,换行符或附件标志符号; 它只会影响后面的字形的布局或定位附件图形。 尽管没有可见的标记,但空格字符通常显示为带有位移的字形。
此方法导致包含指定字形的线段的字形生成和布局,或者如果未启用非连续布局,则直到该线段(包括该线段)。
如果glyphIndex超出范围,则引发NSRangeException。
*/
- (BOOL)notShownAttributeForGlyphAtIndex:(NSUInteger)glyphIndex;

/*
指示字形是否绘制在其线段矩形的外部。

将文本设置为固定的行高时,可能会发生超出范围的情况。例如,如果用户指定12点的固定线高并将字体大小设置为24点,则字形将超过其布局矩形。
此方法导致包含指定字形的线段的字形生成和布局,或者如果未启用非连续布局,则直到该线段(包括该线段)。
在使用rectArrayForCharacterRange:withinSelectedCharacterRange:inTextContainer:rectCount:和rectArrayForGlyphRange:withinSelectedGlyphRange:inTextContainer:rectCount:方法计算封闭矩形时,不考虑绘制在其线段矩形外部的字形。但是,可以通过boundingRectForGlyphRange:inTextContainer:来考虑它们。
*/
- (BOOL)drawsOutsideLineFragmentForGlyphAtIndex:(NSUInteger)glyphIndex;

/*
返回指定标志处附件字形的大小。

对于与附件相对应的字形,此方法返回附件单元格将占据的大小。 如果没有为指定字形设置附件大小,则返回{-1,-1}。
*/
- (CGSize)attachmentSizeForGlyphAtIndex:(NSUInteger)glyphIndex;

/*
返回包含指定索引的线段的截断字形范围。

返回包含指定索引的线段的截断字形范围的范围。 当行片段没有截断时,它将返回{NSNotFound,0}。
*/
- (NSRange)truncatedGlyphRangeInLineFragmentForGlyphAtIndex:(NSUInteger)glyphIndex 

/*
返回指定字符范围生成的字形范围。

如果charRange的长度为0,则所产生的字形范围是紧接在前一个字符对应的字形之后的零长度范围,而ActualCharRange也将为零长度。如果charRange超出文本长度,则该方法将结果截断为文本中字形的数量。
如果未启用非连续布局,则此方法将强制生成所有字符的字形,直到指定范围的末尾(包括该范围)为止。

charRange
返回生成的字形范围的字符范围。
ActualCharRange
如果不为NULL,则在输出时指向完全定义返回的字形范围的字符的实际范围。该范围可以等于或略大于请求的字符范围。例如,如果文本存储区包含字符“ O”和“¨”,而字形存储区包含单个预组合字形“¨Ö”,并且如果charRange仅包含第一个或第二个字符,则将ActualCharRange设置为同时包含两个字符字符。
*/
- (NSRange)glyphRangeForCharacterRange:(NSRange)charRange actualCharacterRange:(nullable NSRangePointer)actualCharRange;

/*
返回与指定字形范围内的字形相对应的字符范围。

如果glyphRange的长度为0,则结果字符范围为零长度范围,紧接在对应于前一个字形的字符之后,而actualGlyphRange也是零长度。如果glyphRange超出了文本长度,则该方法将结果截断为文本中的字符数。
如果未启用非连续布局,则此方法将强制生成所有字符的字形,直到并包括返回范围的末尾。

glyphRange
返回字符范围的字形范围。
actualGlyphRange
如果不为NULL,则在输出时指向由返回的字符范围生成的整个字形范围。此范围可以等于或略大于请求的字形范围。例如,如果文本存储区包含字符“Ö”,而字形缓存包含两个原子字形“ O”和“¨”,并且如果glyphRange仅包含第一个或第二个字形,则将realGlyphRange设置为包含两个字形。
*/
- (NSRange)characterRangeForGlyphRange:(NSRange)glyphRange actualGlyphRange:(nullable NSRangePointer)actualGlyphRange;

/*
返回位于指定文本容器内的字形范围。

与类似的textContainerForGlyphAtIndex:effectiveRange:相比,这是一种效率较低的方法。
根据需要执行字形生成和布局。
*/ 
- (NSRange)glyphRangeForTextContainer:(NSTextContainer *)container;

/*
返回在指定索引处围绕该字形的可显示字形的范围。

返回范围,该范围包括从背面的glyphIndex设置了位置的第一个字形,直到最大范围,但不包括下一个具有位置设置的字形的范围。

glyphIndex
要测试的字形的索引。
*/
- (NSRange)rangeOfNominallySpacedGlyphsContainingIndex:(NSUInteger)glyphIndex;

/*
返回容器中指定字形的边界矩形。

返回最小边界rect(在容器坐标中),它将完全包围给定容器中给定glyphRange中的字形在计算边界矩形之前,将范围与容器的范围相交。此方法可用于将字形范围转换为显示矩形,以在字形范围发生变化时进行无效和重绘。边界矩形始终位于容器坐标中。
根据需要执行字形生成和布局。

glyphRange
要为其返回边界矩形的字形范围。
container
字形在其中放置的文本容器。
*/
- (CGRect)boundingRectForGlyphRange:(NSRange)glyphRange inTextContainer:(NSTextContainer *)container;

/*
返回一个连续的字形范围,其中包含为了绘制所有(甚至部分地)落在给定边界rect中的所有字形而需要显示的所有字形。 此范围可能包含完全不落入矩形的字形。 至多这将返回整个容器的字形范围。 
WithoutAdditionalLayout变体在尝试回答时不会生成字形或执行布局,因此可能不完全正确。 边界矩形始终在容器坐标中。
*/
- (NSRange)glyphRangeForBoundingRect:(CGRect)bounds inTextContainer:(NSTextContainer *)container;
- (NSRange)glyphRangeForBoundingRectWithoutAdditionalLayout:(CGRect)bounds inTextContainer:(NSTextContainer *)container;

/*
返回位于给定容器的坐标系中给定点字形的索引,如果该点下没有字形,则返回最接近的字形。 若要确定该点是否确实在返回的字形范围内,在此之后调用boundingRectForGlyphRange:inTextContainer:并测试该点是否落在该方法返回的rect中。 如果partialFraction为非NULL,它将通过引用返回返回的字形的位置与下一个字形的位置之间的距离的分数。
*/
- (NSUInteger)glyphIndexForPoint:(CGPoint)point inTextContainer:(NSTextContainer *)container fractionOfDistanceThroughGlyph:(nullable CGFloat *)partialFraction;
- (NSUInteger)glyphIndexForPoint:(CGPoint)point inTextContainer:(NSTextContainer *)container;
// 返回指定点的字形与下一个字形之间的距离的分数。
- (CGFloat)fractionOfDistanceThroughGlyphForPoint:(CGPoint)point inTextContainer:(NSTextContainer *)container;

/*
使用指定容器的坐标系返回位于指定点下方的字符的索引。

类似于glyphIndexForPoint:inTextContainer:fractionOfDistanceThroughGlyph :,但以字符索引形式表示。该方法返回落在该点下的字符的索引,以容器的坐标系表示;如果该点下没有字符,则返回最接近的字符。这不仅仅等同于采用相应的字形索引方法的结果并将其转换为字符索引,因为在某些情况下,单个字形表示多个可选字符,例如连字形。在这种情况下,字形中有一个插入点,并且此方法将返回一个字符或另一个字符,这取决于指定点位于该插入点的左侧还是右侧。
通常,此方法仅返回有插入点的字符索引。 partialFraction是从插入点到逻辑上在给定字符之前到下一个字符的距离的一小部分,具体取决于方向性,该字符可以在右边或左边。
*/
- (NSUInteger)characterIndexForPoint:(CGPoint)point inTextContainer:(NSTextContainer *)container fractionOfDistanceBetweenInsertionPoints:(nullable CGFloat *)partialFraction;

/*
返回指定线段的批量插入点。

允许客户在一次调用中获得线段的所有插入点。调用方通过在其中提供一个字符索引来指定行片段,并可以选择是获取主插入点还是备用插入点,以及它们应按逻辑顺序还是按显示顺序进行。
返回值是返回的插入点数。传入的每个指针都应该为NULL,或者指向足够的内存以容纳与行片段中的插入点一样多的元素(其不能超过字符数+ 1)。传入的positions缓冲区将按指定的顺序填充插入点的位置,传入的charIndexes缓冲区将以相应的字符索引填充。位置指示相对于线段rect的原点的横向偏移。内部缓存用于确保对同一行片段重复调用此方法(其他参数可能具有不同的值)不会比单个调用昂贵得多。

charIndex
行段中一个字符的字符索引。
aFlag
如果是,则返回备用而不是主插入点。
dFlag
如果是,则以显示顺序(而不是逻辑顺序)返回插入点。
positions
在输出时,按照指定的顺序插入点的位置。
charIndexes
在输出时,对应于返回的插入点的字符的索引。
*/
- (NSUInteger)getLineFragmentInsertionPointsForCharacterAtIndex:(NSUInteger)charIndex alternatePositions:(BOOL)aFlag inDisplayOrder:(BOOL)dFlag positions:(nullable CGFloat *)positions characterIndexes:(nullable NSUInteger *)charIndexes;

/*
枚举与指定字形范围相交的线段。

此方法导致包含指定范围内的字形的线段的字形生成和布局,或者如果未启用非连续布局,则直到该线段(包括该线段)的所有文本。
线段矩形始终位于容器坐标中。

glyphRange
返回线段矩形的字形范围。
block
适用于字形范围的块。该块具有五个参数:
rect
当前线段矩形。
usedRect
线段矩形的一部分,实际上包含绘制的字形或其他标记(包括文本容器的线段填充)。
textContainer
字形在其中放置的文本容器。
glyphRange
当前行片段中布置的字形范围。
stop
对布尔值的引用。该块可以将值设置为“是”以停止对该数组的进一步处理。 stop参数是仅输出参数。您仅应在块内将此布尔值设置为YES。
*/
- (void)enumerateLineFragmentsForGlyphRange:(NSRange)glyphRange usingBlock:(void (^)(CGRect rect, CGRect usedRect, NSTextContainer *textContainer, NSRange glyphRange, BOOL *stop))block

/*
枚举文本容器中指定字形范围的包围矩形。
*/
- (void)enumerateEnclosingRectsForGlyphRange:(NSRange)glyphRange withinSelectedGlyphRange:(NSRange)selectedRange inTextContainer:(NSTextContainer *)textContainer usingBlock:(void (^)(CGRect rect, BOOL *stop))block

/*
这些方法是用于绘制。 可以覆盖这些内容以执行其他绘图,或完全替换文本绘图,但不能更改布局。 您可以根据需要调用它们,但是焦点必须已经锁定在目标视图或图像上。 -drawBackgroundForGlyphRange:atPoint:绘制文本显示的背景颜色和选择以及标记的范围方面,以及块装饰(例如表格背景和边框)。 -drawGlyphsForGlyphRange:atPoint:绘制实际字形,包括附件以及任何下划线或删除线。 无论哪种情况,所有指定的字形都必须位于单个容器中。
*/
- (void)drawBackgroundForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin;
- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin;

/*
这是字形渲染图元方法的原语。 在CGContext中的位置渲染字形。 这些位置在用户空间坐标系中。 
传入的CGContext已根据文本属性参数(字体,textMatrix和属性)进行配置。 
font参数代表应用于图形状态的字体。 由于系统会自动执行各种字体替换,因此该值可能与attributes参数中的NSFontAttributeName值不同。
 textMatrix是将文本空间坐标系映射到用户空间坐标系的仿射变换。 由于Quartz使用字形位置覆盖它们,因此将忽略textMatrix的tx和ty组件。
*/
- (void)showCGGlyphs:(const CGGlyph *)glyphs positions:(const CGPoint *)positions count:(NSInteger)glyphCount font:(UIFont *)font textMatrix:(CGAffineTransform)textMatrix attributes:(NSDictionary *)attributes inContext:(CGContextRef)CGContext 

/*
这是-drawBackgroundForGlyphRange:atPoint所使用的原语,用于实际使用特定背景色填充矩形,无论是由于背景色属性,选定或标记的范围突出显示,块装饰还是该方法所需的任何其他矩形填充。 
与-showCGGlyphs:...一样,字符范围和颜色仅用于提供信息; 颜色将已经在图形状态下设置。 如果出于任何原因修改了它,则必须先还原它,然后再从此方法返回。 您永远不要调用此方法,但可以覆盖它。 默认实现将仅填充指定的rect数组。
*/
- (void)fillBackgroundRectArray:(const CGRect *)rectArray count:(NSUInteger)rectCount forCharacterRange:(NSRange)charRange color:(UIColor *)color

/*
第一个方法实际上为给定的字形范围绘制了适当的下划线。 
第二个方法可能会将给出的范围细分为子范围,并为实际上应绘制下划线的范围调用drawUnderline...。 
作为为什么有两种方法的示例,请考虑两种情况。 
首先,在所有情况下,您都不想在一行上加下划线。 -underlineGlyphRange ...方法将传递带下划线的字形范围,但是它将寻找前导和尾随空格,并且仅将应加下划线的范围传递给-drawUnderline ...
其次,如果underlineType :表示只应在单词上加下划线(即,没有空格),然后-underlineGlyphRange ...将分割它传递给单词的范围,并且仅将单词范围传递给-drawUnderline。
*/
- (void)drawUnderlineForGlyphRange:(NSRange)glyphRange underlineType:(NSUnderlineStyle)underlineVal baselineOffset:(CGFloat)baselineOffset lineFragmentRect:(CGRect)lineRect lineFragmentGlyphRange:(NSRange)lineGlyphRange containerOrigin:(CGPoint)containerOrigin;
- (void)underlineGlyphRange:(NSRange)glyphRange underlineType:(NSUnderlineStyle)underlineVal lineFragmentRect:(CGRect)lineRect lineFragmentGlyphRange:(NSRange)lineGlyphRange containerOrigin:(CGPoint)containerOrigin;

/*
这两个方法与两个相应的下划线方法平行,但是绘制的是删除线而不是下划线。
*/
- (void)drawStrikethroughForGlyphRange:(NSRange)glyphRange strikethroughType:(NSUnderlineStyle)strikethroughVal baselineOffset:(CGFloat)baselineOffset lineFragmentRect:(CGRect)lineRect lineFragmentGlyphRange:(NSRange)lineGlyphRange containerOrigin:(CGPoint)containerOrigin;
- (void)strikethroughGlyphRange:(NSRange)glyphRange strikethroughType:(NSUnderlineStyle)strikethroughVal lineFragmentRect:(CGRect)lineRect lineFragmentGlyphRange:(NSRange)lineGlyphRange containerOrigin:(CGPoint)containerOrigin;

@protocol NSLayoutManagerDelegate 

/*
每当layoutManager将要通过-setGlyphs:properties:characterIndexes:forGlyphRange:存储初始字形信息时,就会发送此消息。
 此方法允许自定义初始字形生成过程。 它可以使用已修改的字形信息来调用-setGlyphs:properties:characterIndexes:forGlyphRange:。
返回值指定此方法中存储的实际字形范围。 
返回0,它可以指示layoutManager进行默认处理。 请注意,查询glyphRange周围的字形信息可能会导致递归,因为该数据可能尚不可用。
*/
- (NSUInteger)layoutManager:(NSLayoutManager *)layoutManager shouldGenerateGlyphs:(const CGGlyph *)glyphs properties:(const NSGlyphProperty *)props characterIndexes:(const NSUInteger *)charIndexes font:(UIFont *)aFont forGlyphRange:(NSRange)glyphRange

/*
在布局每一行时调用这些方法。 它们允许NSLayoutManager委托自定义线条的形状。
*/
// 返回以glyphIndex结尾的行之后的间距。
- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex withProposedLineFragmentRect:(CGRect)rect

// 返回以glyphIndex开头的行之前的段落间距。
- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager paragraphSpacingBeforeGlyphAtIndex:(NSUInteger)glyphIndex withProposedLineFragmentRect:(CGRect)rect 


// 返回以glyphIndex结尾的行之后的段落间距
- (CGFloat)layoutManager:(NSLayoutManager *)layoutManager paragraphSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex withProposedLineFragmentRect:(CGRect)rect

// 返回charIndex处控制字符的控制字符动作。
- (NSControlCharacterAction)layoutManager:(NSLayoutManager *)layoutManager shouldUseAction:(NSControlCharacterAction)action forControlCharacterAtIndex:(NSUInteger)charIndex

// 确定软线断点时调用。 如果为否,则NSLayoutManager尝试在charIndex之前找到下一个换行符的机会
- (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByWordBeforeCharacterAtIndex:(NSUInteger)charIndex

//在确定连字符点时调用。 如果为否,则NSLayoutManager尝试在charIndex之前找到下一个断字的机会
- (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldBreakLineByHyphenatingBeforeCharacterAtIndex:(NSUInteger)charIndex 

// 用于解决NSControlCharacterWhitespaceAction控件字符的字形指标而调用。
- (CGRect)layoutManager:(NSLayoutManager *)layoutManager boundingBoxForControlGlyphAtIndex:(NSUInteger)glyphIndex forTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)proposedRect glyphPosition:(CGPoint)glyphPosition characterIndex:(NSUInteger)charIndex 

// 允许NSLayoutManagerDelegate在提交到布局缓存之前自定义线段几何。 此方法的实现应确保修改后的片段在文本容器坐标内仍然有效。 当返回YES时,布局管理器使用修改后的矩形。 否则,它将忽略从此方法返回的rect。
- (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldSetLineFragmentRect:(inout CGRect *)lineFragmentRect lineFragmentUsedRect:(inout CGRect *)lineFragmentUsedRect baselineOffset:(inout CGFloat *)baselineOffset inTextContainer:(NSTextContainer *)textContainer forGlyphRange:(NSRange)glyphRange


/************************ Layout processing ************************/
// 每当布局或字形在先前已完成所有布局的布局管理器中失效时,将发送此消息。
- (void)layoutManagerDidInvalidateLayout:(NSLayoutManager *)sender 

// 每当容器已装满时发送。 此方法对分页很有用。 如果我们完成了所有布局,但并非所有布局都适合现有容器,则textContainer可能为nil。 atEnd标志指示所有布局是否完整。
- (void)layoutManager:(NSLayoutManager *)layoutManager didCompleteLayoutForTextContainer:(nullable NSTextContainer *)textContainer atEnd:(BOOL)layoutFinishedFlag

//这是由于textContainer更改几何形状而在layoutManager使布局无效之前立即发送的。 此方法的接收者可以对几何形状变化做出反应并执行调整,例如重新创建排除路径。
- (void)layoutManager:(NSLayoutManager *)layoutManager textContainer:(NSTextContainer *)textContainer didChangeGeometryFromSize:(CGSize)oldSize 
@end
  • CoreText:没有直接包含在 TextKit 中,CoreText 是进行实际排版的库。对于布局管理器的每一步,CoreText 被这样或那样的方式调用。它提供了从字符到字形的翻译,用它们来填充行,以及建议断字点。

正如上面 TextKit 简图中表明,NSTextStorage、NSLayoutManager 和 NSTextContainer 之间的箭头表明它们的关系是 1 对 N 的关系 : 一个 Text Storage 可以拥有多个 Layout Manager,一个 Layout Manager 也可以拥有多个 Text Container。这些多重性带来了很好的特性:

  • 多个 Layout Manager 附加到同一个 Text Storage 上,可以产生相同文本的多种视觉表现,而且可以把放到一起来显示。每一个表现都有独立的位置和大小。如果相应的 Text View 可编辑,那么在某个 Text View 上做的所有修改都会马上反映到所有 Text View 上。

  • 将多个 Text Container 附加到同一个 Layout Manager 上,这样可以将一个文本分布到多个视图展现出来。eg:基于页面的布局,每个页面包含一个单独的 Text View。所有这些视图的 Text Container 都引用同一个 Layout Manager,这时这个 Layout Manager 就可以将文本分布到这些视图上来显示。

使用实例

共享文本

三个UITextView,其中一个textView的文本分布到另外两个textView上

手工创建一个文本系统
NSTextStorage *textStorage = [NSTextStorage new];

NSLayoutManager *layoutManager = [NSLayoutManager new];
[textStorage addLayoutManager: layoutManager];

NSTextContainer *textContainer = [NSTextContainer new];
[layoutManager addTextContainer: textContainer];

UITextView *textView = [[UITextView alloc] initWithFrame:someFrame 
 textContainer:textContainer];

必须强引用 这个 Text Storage。在栈底的 Text View 只保留了对 Text Storage 和 Layout Manager 的弱引用。当 Text Storage 被释放时,Layout Manager 也被释放了,这样留给 Text View 的就只有一个断开的 Text Container 了。

将第二个 Text Container 附加到 Layout Manager 也差不多。比方说我们希望上面例子中的文本填充两个 Text View,而非一个。

NSTextContainer *thirdTextContainer = [NSTextContainer new];
[otherLayoutManager addTextContainer: thirdTextContainer];

UITextView *thirdTextView = [[UITextView alloc] initWithFrame:someFrame
                                                textContainer:thirdTextContainer];

由于在 otherTextView 中的 Text Container 可以无限地调整大小,thirdTextView 永远不会得到任何文本。因此,我们必须指定文本应该从一个视图回流到其它视图,而不应该调整大小或者滚动:

otherTextView.scrollEnabled = NO;

不幸的是,看来将多个 Text Container 附加到一个 Layout Manager 会禁用编辑功能。如果必须保留编辑功能的话,那么一个 Text Container 只能附加到一个 Layout Manager 上。

全部代码

// 加载文本
    NSTextStorage *sharedTextStorage = self.originalTextView.textStorage;
    [sharedTextStorage replaceCharactersInRange:NSMakeRange(0, 0) withString:[NSString stringWithContentsOfURL:[NSBundle.mainBundle URLForResource:@"lorem" withExtension:@"txt"] usedEncoding:NULL error:NULL]];
    
    
    // 创建两个 layoutManager
    NSLayoutManager *otherLayoutManager = [NSLayoutManager new];
    [sharedTextStorage addLayoutManager: otherLayoutManager];
    
    // 创建两个TextContainer和两个textView
    // 第一个textView禁止滚动
    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;
        
    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;

语法高亮

看看 TextKit 组件的责任划分,就很清楚语法高亮应该由 Text Storage 实现。因为 NSTextStorage 是一个类簇,创建它的子类需要做不少工作。我的想法是建立一个复合对象:实现所有的方法,但只是将对它们的调用转发给一个实际的实例,将输入输出参数或者结果修改为希望的样子。

NSTextStorage 继承自 NSMutableAttributedString,并且必须实现以下四个方法:

- (NSString *)string;
- (NSDictionary *)attributesAtIndex:(NSUInteger)location
                     effectiveRange:(NSRangePointer)range;
- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str;
- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range;

一个类簇的子类的复合对象的实现也相当简单。首先,找到一个满足所有要求的最简单的类。在我们的例子中,它是 NSMutableAttributedString,我们用它作为实现NSTextStorage的实现:

@implementation TKDHighlightingTextStorage
{
    NSMutableAttributedString *_imp;
}

- (id)init
{
    self = [super init];
    if (self) {
        _imp = [NSMutableAttributedString new];
    }
    return self;
}

有了这个对象,只需要一行代码就可以实现两个 getter 方法:

- (NSString *)string
{
    return _imp.string;
}

- (NSDictionary *)attributesAtIndex:(NSUInteger)location effectiveRange:(NSRangePointer)range
{
    return [_imp attributesAtIndex:location effectiveRange:range];
}

实现两个 setter 方法也几乎同样简单。但Text Storage 需要通知它的 Layout Manager 变化发生了。因此 settter 方法必须也要调用 -edited:range:changeInLegth: 并传给它变化的描述。

- (void)replaceCharactersInRange:(NSRange)range withString:(NSString *)str
{
    [_imp replaceCharactersInRange:range withString:str];
    [self edited:NSTextStorageEditedCharacters range:range
                                      changeInLength:(NSInteger)str.length - (NSInteger)range.length];
}

- (void)setAttributes:(NSDictionary *)attrs range:(NSRange)range
{
    [_imp setAttributes:attrs range:range];
    [self edited:NSTextStorageEditedAttributes range:range changeInLength:0];
}

我们希望将所有 iWords 的颜色变成红色——也就是那些以小写“i”开头,后面跟着一个大写字母的单词。

一个方便实现高亮的办法是覆盖 -processEditing。每次文本存储有修改时,这个方法都自动被调用。每次编辑后,NSTextStorage 会用这个方法来清理字符串。例如,有些字符无法用选定的字体显示时,Text Storage 使用一个可以显示它们的字体来进行替换。

和其它一样,为 iWords 增加一个简单的高亮也相当简单。我们覆盖 -processEditing,调用父类的实现,并设置一个正则表达式来查找单词:

- (void)processEditing
{
    //创建正则表达式
    static NSRegularExpression *iExpression;
    iExpression = iExpression ?: [NSRegularExpression regularExpressionWithPattern:@"i[\\p{Alphabetic}&&\\p{Uppercase}][\\p{Alphabetic}]+" options:0 error:NULL];
    
    
    // 清除文字颜色
    NSRange paragaphRange = [self.string paragraphRangeForRange: self.editedRange];
    [self removeAttribute:NSForegroundColorAttributeName range:paragaphRange];
    
    // 匹配字符
    [iExpression enumerateMatchesInString:self.string options:0 range:paragaphRange usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
        // 添加红色
        [self addAttribute:NSForegroundColorAttributeName value:[UIColor redColor] range:result.range];
    }];
  
  // 调用父类方法,将最终确定属性并调用委托方法。
  [super processEditing];
}

外界调用

// Replace text storage
_textStorage = [TKDHighlightingTextStorage new];
[_textStorage addLayoutManager: self.textView.layoutManager];

布局修改

Layout Manager 是核心的布局主力。虽然 TextKit 基于性能方面的考虑,不具备像 Cocoa 文本系统那样的完全可定制性,但它提供很多代理方法来允许做一些调整。
TextKit的理念:“用简单但是高性能的方法实现了这个功能。且提供一些方法去更改它的一些东西。但只能在不太损害性能的地方进行修改。”

行间距
- (CGFloat)      layoutManager:(NSLayoutManager *)layoutManager
  lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex
  withProposedLineFragmentRect:(CGRect)rect
{
    return floorf(glyphIndex / 100);
}
链接不折行

检测链接

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

有了这个,改变断行行为就只需要实现一个 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));   

文本交互

前面的实例涉及到了 NSTextStorage 和 NSLayoutManager,最后一个演示程序将涉及 NSTextContainer。

需求是不将文本放置在某些区域,

对于这种情况,iOS 上的 NSTextContainer 提供了一个属性:exclusionPaths,它允许开发者设置一个 NSBezierPath 数组来指定不可填充文本的区域。

在 Text View 里面实现这个行为很简单,但是Bezier Path 的坐标必须使用容器的坐标系。

- (void)updateExclusionPaths
{
// 获得坐标
    CGRect ovalFrame = [self.textView convertRect:self.circleView.bounds fromView:self.circleView];
    ovalFrame.origin.x -= self.textView.textContainerInset.left;
    ovalFrame.origin.y -= self.textView.textContainerInset.top;
    
    // 设置路径
    UIBezierPath *ovalPath = [UIBezierPath bezierPathWithOvalInRect: ovalFrame];
    self.textView.textContainer.exclusionPaths = @[ovalPath];
}

你可能感兴趣的:(iOS 文本相关-TextKit`)