TextKit框架详细解析 (十四) —— 文本编程指南之更底层的文字处理技术 (十)

版本记录

版本号 时间
V1.0 2018.09.02

前言

TextKit框架是对Core Text的封装,用简洁的调用方式实现了大部分Core Text的功能。 TextKit是一个偏上层的开发框架,在iOS7以上可用,使用它可以方便灵活处理复杂的文本布局,满足开发中对文本布局的各种复杂需求。TextKit实际上是基于CoreText的一个上层框架,其是面向对象的。接下来几篇我们就一起看一下这个框架。感兴趣的看下面几篇文章。
1. TextKit框架详细解析 (一) —— 基本概览和应用场景(一)
2. TextKit框架详细解析 (二) —— 基本概览和应用场景(二)
3. TextKit框架详细解析 (三) —— 一个简单布局示例(一)
4. TextKit框架详细解析 (四) —— 一个简单布局示例(二)
5. TextKit框架详细解析 (五) —— 文本编程指南之简介(一)
6. TextKit框架详细解析 (六) —— 文本编程指南之展示文本内容(二)
7. TextKit框架详细解析 (七) —— 文本编程指南之排版概念(三)
8. TextKit框架详细解析 (八) —— 文本编程指南之管理Text Fields and Text Views(四)
9. TextKit框架详细解析 (九) —— 文本编程指南之管理键盘(五)
10. TextKit框架详细解析 (十) —— 文本编程指南之复制、剪切和粘贴操作(六)
11. TextKit框架详细解析 (十一) —— 文本编程指南之输入数据的自定义视图(七)
12. TextKit框架详细解析 (十二) —— 文本编程指南之展示和管理编辑菜单(八)
13. TextKit框架详细解析 (十三) —— 文本编程指南之使用TextKit绘制和管理文本(九)

Lower Level Text-Handling Technologies - 更底层的文字处理技术

大多数应用程序可以使用高级文本显示类和Text Kit进行所有文本处理。 但是,您可能需要一个应用程序,它需要来自Core TextCore GraphicsCore Animation框架的更底层的编程接口以及UIKit本身的其他API。


Simple Text Drawing - 简单文本绘制

除了用于显示和编辑文本的UIKit类之外,iOS还包括几种直接在屏幕上绘制文本的方法。绘制简单字符串的最简单,最有效的方法是使用UIKit附加的NSString类,该类位于名为UIStringDrawing的类别中。这些扩展包括使用各种属性在屏幕上绘制字符串的方法。还有一些方法可以在实际绘制之前计算渲染字符串的大小,这可以帮助您更精确地布置应用内容。

重要提示:有一些很好的理由可以避免直接使用文本来支持使用UIKit框架的文本对象。一个是性能performance。虽然,UILabel对象也绘制其静态文本,但它只绘制一次,而文本绘制例程通常重复调用。文本对象也提供更多的交互;例如,它们是可选择的。

UIStringDrawing的方法在给定点(对于单行文本)或在指定矩形(对于多行)中绘制字符串。您可以传入绘图中使用的属性,例如字体,换行模式和基线调整。 UIStringDrawing的方法允许您精确调整渲染文本的位置,并将其与视图内容的其余部分混合。它们还允许您根据所需的字体和样式属性预先计算文本的边界矩形。

您还可以使用Core Animation的CATextLayer类的来进行简单的文本绘制。此类的对象将纯字符串或属性字符串存储为其内容,并提供一组影响该内容的属性,如字体,字体大小,文本颜色和截断行为。 CATextLayer的优点是(作为CALayer的子类),其属性本身就具有动画功能。 Core AnimationQuartzCore框架相关联。由于CATextLayer的实例知道如何在当前图形上下文中绘制自己,因此在使用这些实例时无需发出任何显式绘图命令。

有关NSString的字符串绘制扩展的信息,请参阅NSString UIKit Additions Reference。要了解有关CATextLayerCALayer和其他类Core Animation的更多信息,请阅读Core Animation Programming Guide


Core Text

Core Text是一种用于自定义文本布局和字体管理的技术。应用程序开发人员通常不需要直接使用Core Text。 Text Kit构建于Core Text之上,具有相同的优势,例如速度和复杂的排版功能。此外,Text Kit提供了大量基础结构,如果您使用Core Text,则必须为自己构建。

但是,必须直接使用Core Text API的时候,开发人员才会使用它。它旨在由具有自己的布局引擎的应用程序使用 - 例如,具有自己的页面布局引擎的文字处理器可以使用Core Text生成字形并将它们相对于彼此定位。

Core Text是作为一个框架实现的,该框架发布类似于Core Foundation的API,类似于它是程序性的(ANSI C),但它基于类似对象的不透明类型。此API与Core FoundationCore Graphics集成在一起。例如,Core Text在许多输入和输出参数中使用Core FoundationCore Graphics对象。此外,由于许多Core Foundation对象与Foundation框架中的对应的对象是自由桥接的,因此您可以在Core Text函数的参数中使用一些Foundation对象。

注意:如果使用Core TextCore Graphics绘制文本,请记住必须将翻转变换应用于当前文本矩阵,以使文本以正确的方向显示 - 即绘图原点位于左上角字符串的边界框。

Core Text有两个主要部分:布局引擎和字体技术,每个部分都由自己的不透明类型集合支持。

1. Core Text Layout Opaque Types - Core Text布局不透明类型

Core Text需要两个对象,其不透明类型不是原生的:属性字符串(CFAttributedStringRef)和图形路径(CGPathRef)。属性字符串对象封装支持显示文本的字符串,并包含定义字符串中字符的样式方面的properties(或attributes),例如字体和颜色。图形路径定义文本框架的形状,相当于一个段落。

运行时的Core Text对象形成一个层次结构,反映了正在处理的文本的级别(参见图9-1)。在此层次结构的顶部是框架集对象(CTFramesetterRef)。使用属性字符串和图形路径作为输入,框架集生成一个或多个文本框架(CTFrameRef)。由于文本在框架中布局,框架设置会对其应用段落样式,包括对齐,制表位,行间距,缩进和换行模式等属性。

为了生成帧framesframesetter调用排版对象(CTTypesetterRef)。typesetter将属性字符串中的字符转换为字形,并将这些字形放入填充文本框的行中。 (字形是用于表示字符的图形形状。)帧中的一条线由CTLine对象(CTLineRef)表示。 CTFrame对象包含CTLine对象的数组。

反过来,CTLine对象包含一个字形运行数组,由CTRunRef类型的对象表示。字形运行是一系列具有相同属性和方向的连续字形。虽然typesetter对象返回CTLine对象,但它会从字形运行数组合成这些行。

TextKit框架详细解析 (十四) —— 文本编程指南之更底层的文字处理技术 (十)_第1张图片
Figure 9-1 Core Text layout objects

使用CTLine 不透明类型的函数,您可以从属性字符串中绘制一行文本,而无需通过CTFramesetter对象。 您只需将文本的原点定位在文本基线上,并请求线对象绘制自己。

2. Core Text Font Opaque Types - Core Text字体不透明类型

字体对于Core Text中的文本处理至关重要。typesetter对象使用字体(以及源属性字符串)来转换字符中的字形,然后将这些字形相对于彼此放置。图形上下文是Core Text中字体的核心。您可以使用图形上下文函数来设置当前字体和绘制字形;或者您可以从属性字符串创建CTLine对象,并使用其函数绘制到图形上下文中。 Core Text字体系统本身处理Unicode字体。

字体系统包括三种不透明类型的对象:CTFontCTFontDescriptorCTFontCollection

  • 字体对象(CTFontRef)使用点大小和特定特征(来自转换矩阵)进行初始化。您可以查询字体对象的字符到字形映射,其编码,字形数据以及ascent, leading等指标。 Core Text还提供了一种名为font cascading的自动字体替换机制。
  • 字体描述符对象(CTFontDescriptorRef)通常用于创建字体对象。它们允许您指定包含PostScript名称,字体系列和样式以及特征(例如,粗体或斜体)等属性的字体属性字典,而不是处理复杂的转换矩阵。
  • 字体集合对象(CTFontCollectionRef)是字体描述符组,提供字体枚举和对全局和自定义字体集合的访问等服务。

可以通过调用CTFontCreateWithName将UIFont对象转换为CTFont对象,并传递由UIFont对象封装的字体名称和点大小。


Core Graphics Text Drawing - Core Graphics文本绘制

Core Graphics(或Quartz)是处理最低级别二维成像的系统框架。 文本绘图是其功能之一。 通常,由于Core Graphics较底层,因此建议您使用系统的其他技术之一来绘制文本。 但是,如果情况需要,您可以使用Core Graphics绘制文本。

您可以使用CGContext不透明类型的函数选择字体,设置文本属性和绘制文本。 例如,您可以调用CGContextSelectFont来设置使用的字体,然后调用CGContextSetFillColor来设置文本颜色。 然后设置文本矩阵(CGContextSetTextMatrix)并使用CGContextShowGlyphsAtPoint绘制文本。

有关这些函数及其用法的更多信息,请参阅Quartz 2D Programming GuideCore Graphics Framework Reference


Foundation-Level Regular Expressions - Foundation级正则表达式

Foundation框架的NSString类包含一个用于正则表达式的简单编程接口。 您可以调用返回范围的三种方法之一,传入特定的选项常量和正则表达式字符串。 如果匹配,则该方法返回子字符串的范围。 该选项是NSRegularExpressionSearch常量,它是位掩码类型NSStringCompareOptions。此常量告诉方法期望正则表达式模式而不是文字字符串作为搜索值。 支持的正则表达式语法是由ICU(International Components for Unicode)定义的语法。

注意:除了此处描述的NSString正则表达式功能之外,iOS还为NSRegularExpression类的正则表达式提供了更完整的支持。 ICU User Guide描述了如何构建ICU正则表达式(http://userguide.icu-project.org/strings/regexp)。

正则表达式的NSString方法如下:

  • rangeOfString:options:
  • rangeOfString:options:range:
  • rangeOfString:options:range:locale:

如果在这些方法中指定NSRegularExpressionSearch选项,则可以指定的唯一其他NSStringCompareOptions选项是NSCaseInsensitiveSearch和NSAnchoredSearch。 如果正则表达式搜索未找到匹配项或者正则表达式语法格式错误,则这些方法将返回值为{NSNotFound,0}NSRange结构。

Listing 9-1给出了使用NSString正则表达式API的示例。

// Listing 9-1  Finding a substring using a regular expression

    // finds phone number in format nnn-nnn-nnnn
    NSRange r;
    NSString *regEx = @"[0-9]{3}-[0-9]{3}-[0-9]{4}";
    r = [textView.text rangeOfString:regEx options:NSRegularExpressionSearch];
    if (r.location != NSNotFound) {
        NSLog(@"Phone number is %@", [textView.text substringWithRange:r]);
    } else {
        NSLog(@"Not found.");
    }

因为这些方法为匹配模式的子字符串返回单个range值,所以ICU库的某些正则表达式功能要么不可用,要么必须以编程方式添加。 此外,NSStringCompareOptions选项(如向后搜索,数字搜索和变音不敏感搜索)不可用,并且不支持捕获组。

在测试返回range时,您应该了解基于文字字符串的搜索和基于正则表达式模式的搜索之间的某些行为差异。 某些模式可以成功匹配并返回长度为0的NSRange结构(在这种情况下,感兴趣的是location字段)。 其他模式可以成功匹配空字符串,或者在带有range参数的方法中,可以与零长度搜索范围匹配。


ICU Regular-Expression Support - ICU正则表达式支持

如果NSString对正则表达式的支持不足以满足您的需求,则ICU 4.2.1中的库的修改版本将包含在系统的BSD(非框架)级别的iOS中。 ICU(International Components for Unicode)是一个用于Unicode支持和软件国际化的开源项目。 已安装的ICU版本包括支持正则表达式所需的头文件,以及与这些接口相关的一些修改,即:

parseerr.h
platform.h
putil.h
uconfig.h
udraft.h
uintrnal.h
uiter.h
umachine.h
uregex.h
urename.h
ustring.h
utf_old.h
utf.h
utf16.h
utf8.h
utypes.h
uversion.h

您可以在http://icu-project.org/apiref/icu4c/index.html上阅读ICU 4.2 API文档和用户指南。


Simple Text Input - 简单文本输入

想要显示和处理文本的应用程序不仅限于UIKit框架的文本和Web对象。它可以实现自定义视图,从简单的文本输入到复杂的文本处理和自定义输入。通过可用的编程接口,这些应用程序可以获取自定义文本布局,多级输入,自动更正,自定义键盘和拼写检查等功能。

您可以实现自定义视图,允许用户在插入点处输入文本,并在点击删除键时删除插入点之前的字符。例如,即时消息应用程序可以具有允许用户输入他们的对话部分的视图。

您可以通过继承UIView或继承自UIResponder的任何其他视图类并采用UIKeyInput协议来获取此功能以进行简单的文本输入。当视图类的实例成为第一个响应者时,UIKit会显示系统键盘。 UIKeyInput本身采用UITextInputTraits协议,因此您可以设置键盘类型,返回键类型和键盘的其他属性。

注意:只有一部分可用的键盘和语言可用于仅采用UIKeyInput协议的类。例如,排除任何多阶段输入方法,例如中文,日文,韩文和泰文。如果类也采用UITextInput协议,则可以使用那些输入方法。

要采用UIKeyInput,必须实现它声明的三个方法:hasText, insertText:和deleteBackward。要进行文本的实际绘制,您可以使用本章中概述的任何技术。但是,对于简单的文本输入,例如自定义控件中的单行文本,UIStringDrawingCATextLayer API是最合适的。

Listing 9-2说明了自定义视图类的UIKeyInput实现。此示例中的textStore属性是一个NSMutableString对象,用作文本的后备存储。实现可以追加或删除字符串中的最后一个字符(取决于是否按下了字母数字键或Delete键),然后重绘textStore

// Listing 9-2  Implementing simple text entry

- (BOOL)hasText {
    if (textStore.length > 0) {
        return YES;
    }
    return NO;
}
 
- (void)insertText:(NSString *)theText {
    [self.textStore appendString:theText];
    [self setNeedsDisplay];
}
 
- (void)deleteBackward {
    NSRange theRange = NSMakeRange(self.textStore.length-1, 1);
    [self.textStore deleteCharactersInRange:theRange];
    [self setNeedsDisplay];
}
 
- (void)drawRect:(CGRect)rect {
    CGRect rectForText = [self rectForTextWithInset:2.0]; // custom method
    [self.theColor set];
    UIRectFrame(rect);
    [self.textStore drawInRect:rectForText withFont:self.theFont];
}

要在视图中实际绘制文本,此代码使用来自NSString上的UIStringDrawing类别的drawInRect:withFont:。


Communicating with the Text Input System - 文本输入系统的通信

iOS的文本输入系统管理键盘。它将分接头解释为适用于某些语言的特定键盘中特定键的按下。然后,它将关联的字符发送到目标视图以进行插入。如Simple Text Input中所述,视图类必须采用UITextInput协议在插入符(插入点)插入和删除字符。

但是,文本输入系统不仅仅是简单的文本输入。例如,它管理自动更正和多级输入,它们都基于当前选择和上下文。汉语(日语)和汉字(汉语)等表意语言需要多级文本输入,它们从语音键盘输入。要获取这些功能,自定义文本视图必须通过采用UITextInput协议并实现相关的客户端类和协议与文本输入系统进行通信。

以下部分描述了与文本输入系统通信的自定义文本视图的一般需要做的事情。 A Guided Tour of a UITextInput Implementation检查UITextInput的典型实现中最重要的类和方法。

1. Overview of the Client Side of Text Input - 文本输入的客户端概述

想要与文本输入系统通信的类必须采用UITextInput协议。该类需要从UIResponder继承,并且在大多数情况下是自定义视图。

注意:采用UITextInput的响应者类不必是绘制和管理文本的视图(如在A Guided Tour of a UITextInput Implementation中的情况)。但是,如果它不是绘制和管理文本的视图,那么采用UITextInput的类应该能够直接与视图进行通信。为简单起见,以下讨论引用了采用UITextInput作为文本视图的响应者类。

文本视图必须有自己的文本布局和字体管理;为此,建议使用Core Text框架。 (Core Text概述了Core Text。)该类还应采用并实现UIKeyInput协议,并应设置UITextInputTraits协议的必要属性。

文本输入系统的客户端和系统端的一般体系结构如图9-2所示。

TextKit框架详细解析 (十四) —— 文本编程指南之更底层的文字处理技术 (十)_第2张图片
Figure 9-2 Paths of communication with the text input system

文本输入系统调用文本视图实现的UITextInput方法。其中许多方法从文本视图中请求有关特定文本位置和文本范围的信息,并在其他方法调用中将相同的信息传递回类。在Tasks of a UITextInput Object中总结了这些文本位置和文本范围交换的原因。

文本输入系统中的文本位置和文本范围由自定义类的实例表示。Text Positions and Text Ranges更详细地讨论了这些对象。

文本视图还维护对tokenizer和输入代理的引用。文本视图调用UITextInputDelegate协议声明的方法,以通知系统提供的输入代理有关文本和选择中的外部更改。文本输入系统与tokenizer对象通信以确定文本单元的粒度 - 例如,字符,单词和段落。 tokenizer是一个采用UITextInputTokenizer协议的对象。文本视图包含一个属性(由UITextInput声明),该属性包含对tokenizer的引用。

Text Positions and Text Ranges - 文本位置和文本区间

客户端应用程序必须创建两个类,其实例表示文本视图中文本的位置和范围。这些类必须是UITextPosition和UITextRange的子类。

虽然UITextPosition本身没有声明方法或属性,但它是文本文档和文本输入系统之间交换信息的重要部分。文本输入系统需要一个对象来表示文本中的位置,而不是整数或结构。此外,当支持文本的字符串与该位置具有不同的偏移量时,UITextPosition对象可以通过表示可见文本中的位置来实现实际目的。当字符串包含不可见的格式字符(例如RTF和HTML文档)或嵌入的对象(例如附件)时,会发生这种情况。在查找可见字符的字符串偏移时,自定义UITextPosition类可以考虑这些不可见字符。在最简单的情况下 - 没有嵌入对象的纯文本文档 - 自定义UITextPosition对象可以封装单个偏移量或索引整数。

UITextRange声明一个简单的接口,其中两个属性是开始和结束自定义UITextPosition对象。第三个属性包含一个布尔值,指示范围是否为空(即没有长度)。

Tasks of a UITextInput Object - UITextInput对象的任务

需要采用UITextInput协议的类来实现协议的大部分方法和属性。除了少数例外,这些方法将自定义UITextPosition或UITextRange对象作为参数或返回其中一个对象。在运行时,文本系统调用这些方法,并且在几乎所有情况下,都会期望返回一些对象或值。

文本视图必须将文本位置分配给标记显示文本开头和结尾的属性。此外,它还必须保持当前所选文本的范围和当前标记文本的范围(如果有)。标记文本是多级文本输入的一部分,表示用户尚未确认的临时插入文本。它以独特的方式设计。标记文本的范围始终包含一系列选定文本,可能是一系列字符或插入符号。

UITextInput对象实现的方法可以分为不同的任务:

  • 按文本范围返回和替换文本。给定范围,返回该范围内的文本或用文本输入系统提供的文本替换该文本。

    • textInRange:
    • replaceRange:withText:
  • 计算文本范围和文本位置。在给定两个文本位置的情况下创建并返回UITextRange对象(或简称为文本范围);或者在给定文本位置和偏移量的情况下创建并返回UITextPosition对象(或简称为文本位置)。

    • positionFromPosition:offset:
    • positionFromPosition:inDirection:offset:
    • textRangeFromPosition:toPosition:
  • 评估文本位置。比较两个文本位置或将偏移从一个文本位置返回到另一个文本位置。

    • comparePosition:toPosition:
    • offsetFromPosition:toPosition:
  • 回答布局问题。通过在给定的布局方向上扩展来确定文本位置或文本范围。

    • positionWithinRange:farthestInDirection:
    • characterRangeByExtendingPosition:inDirection:
  • 命中测试。给定一个点,返回最接近的文本位置或文本范围。

    • closestPositionToPoint:
    • closestPositionToPoint:withinRange:
    • characterRangeAtPoint:
  • 返回文本范围和文本位置的矩形。返回包含文本范围的矩形或插入符号文本位置的矩形。

    • firstRectForRange:
    • caretRectForPosition:

UITextInput对象也可能选择实现一个或多个可选的协议方法。这使它能够返回从指定文本位置开始的文本样式(字体,文本颜色,背景颜色),并协调可见文本位置和字符偏移(对于那些值不相同的UITextPosition对象)。

由于外部原因在文本视图中发生更改 - 也就是说,它们不是由来自文本输入系统的调用引起的 - UITextInput对象应该将textWillChange:textDidChange:selectionWillChange:selectionDidChange:消息发送到输入代理(它提到了引用)。例如,当用户点击文本视图并设置所选文本的范围以将插入点放在手指下时,在更改所选范围之前,您将发送selectionWillChange:,并在更改范围后发送selectionDidChange :

Tokenizers

Tokenizers是确定文本位置是否在具有给定粒度的文本单元的边界内或边界处的对象。当由文本输入系统查询时,Tokenizers返回具有给定粒度的文本单元的范围或具有给定粒度的文本单元的边界文本位置。当前定义的粒度是字符,单词,句子,段落,行和文档。UITextGranularity类型的枚举常量表示这些粒度。始终参考存储或布局方向评估文本单元的粒度。

文本输入系统以各种方式使用Tokenizers。例如,键盘可能需要最后一句话的上下文来确定用户尝试键入的内容。或者,如果用户按下Option-left箭头键(在外部键盘上),文本系统将查询tokenizer以查找移动到上一个单词所需的信息。

tokenizer是符合UITextInputTokenizer协议的类的实例。 UITextInputStringTokenizer类提供适用于所有支持语言的UITextInputTokenizer协议的默认基本实现。如果您需要对不同粒度的文本单元进行全新解释的tokenizer,则应采用UITextInputTokenizer并实现其所有方法。否则,您应该将UITextInputStringTokenizer子类化,以提供有关布局方向的特定于应用程序的信息。

初始化UITextInputStringTokenizer对象时,请为其提供采用UITextInput协议的视图。反过来,UITextInput对象应该在tokenizer属性的getter方法中懒创建其tokenizer对象。

2. A Guided Tour of a UITextInput Implementation - UITextInput实现指导

SimpleTextInput是一个基于Core Text的简单文本编辑应用程序。 它有两个UIView的自定义子类。 一个视图子类SimpleCoreTextView使用Core Text提供文本布局和编辑支持。 另一个视图子类EditableCoreTextView采用UIKeyInput协议来启用文本输入;它还采用UITextInput协议,创建并实现相关的子类与文本输入系统进行通信。 EditableCoreTextViewSimpleCoreTextView嵌入为实例变量,实例化它,并在大多数UITextInputUIKeyInput方法实现中调用它。

注意:出于空间原因,导览显示了最重要或最具说明性的UITextInput方法的实现。 但是,可以从这些选择的实现推断到其他协议。 代码取自SimpleTextInput示例代码项目。

Subclasses of UITextPosition and UITextRange - UITextPosition和UITextRange的子类

EditableCoreTextView创建一个名为IndexedPositionUITextPosition的自定义子类,以及一个名为IndexedRangeUITextRange的自定义子类。 这些子类只根据其中两个索引封装单个索引值和NSRange值。 Listing 9-3显示了这些类的声明。

// Listing 9-3  Declaring the IndexedPosition and IndexedRange classes

@interface IndexedPosition : UITextPosition {
    NSUInteger _index;
    id  _inputDelegate;
}
@property (nonatomic) NSUInteger index;
+ (IndexedPosition *)positionWithIndex:(NSUInteger)index;
@end
 
@interface IndexedRange : UITextRange {
    NSRange _range;
}
@property (nonatomic) NSRange range;
+ (IndexedRange *)rangeWithNSRange:(NSRange)range;
 
@end

这两个类都声明了类工厂方法来实现vend实例。 Listing 9-4显示了这些方法的实现以及UITextRange类声明的方法。

// Listing 9-4  Implementing the IndexedPosition and IndexedRange classes

@implementation IndexedPosition
@synthesize index = _index;
 
+ (IndexedPosition *)positionWithIndex:(NSUInteger)index {
    IndexedPosition *pos = [[IndexedPosition alloc] init];
    pos.index = index;
    return [pos autorelease];
}
 
@end
 
@implementation IndexedRange
@synthesize range = _range;
 
+ (IndexedRange *)rangeWithNSRange:(NSRange)nsrange {
    if (nsrange.location == NSNotFound)
        return nil;
    IndexedRange *range = [[IndexedRange alloc] init];
    range.range = nsrange;
    return [range autorelease];
}
 
- (UITextPosition *)start {
    return [IndexedPosition positionWithIndex:self.range.location];
}
 
- (UITextPosition *)end {
        return [IndexedPosition positionWithIndex:(self.range.location + self.range.length)];
}
 
-(BOOL)isEmpty {
    return (self.range.length == 0);
}
@end

Inserting and Deleting Text - 插入和删除文本

采用UITextInput协议的文本视图也必须采用UIKeyInput协议。 这意味着它必须实现insertText:, deleteBackward和hasText方法,如Simple Text Input中所述。 由于EditableCoreTextView类采用UITextInput,因此在输入和删除文本时,它还必须维护选定和标记的文本范围(即selectedTextRange和markedTextRange属性的当前值)。

Listing 9-5说明了在输入文本时EditableCoreTextView如何执行此操作。 如果输入字符时有标记文本,则通过调用支持可变字符串上的replaceCharactersInRange:withString:方法,将标记文本替换为字符。 如果存在选定的文本范围,则会使用输入字符替换该范围内的字符。 否则,该方法将插入输入文本。

// Listing 9-5  Inserting text input into storage and updating selected and marked ranges

- (void)insertText:(NSString *)text {
    NSRange selectedNSRange = _textView.selectedTextRange;
    NSRange markedTextRange = _textView.markedTextRange;
 
    if (markedTextRange.location != NSNotFound) {
        [_text replaceCharactersInRange:markedTextRange withString:text];
        selectedNSRange.location = markedTextRange.location + text.length;
        selectedNSRange.length = 0;
        markedTextRange = NSMakeRange(NSNotFound, 0);
    } else if (selectedNSRange.length > 0) {
        [_text replaceCharactersInRange:selectedNSRange withString:text];
        selectedNSRange.length = 0;
        selectedNSRange.location += text.length;
    } else {
        [_text insertString:text atIndex:selectedNSRange.location];
        selectedNSRange.location += text.length;
    }
    _textView.text = _text;
    _textView.markedTextRange = markedTextRange;
    _textView.selectedTextRange = selectedNSRange;
}

尽管EditableCoreTextView实现的deleteBackward方法的结构与insertText:方法相同,但是在选择和标记的文本范围的调整方式上存在适当的差异。 另一个区别是在支持可变字符串而不是replaceCharactersInRange:withString:上调用了deleteCharactersInRange:方法。

Returning and Replacing Text by Range - 按范围返回和替换文本

与文本输入系统通信的任何文本视图必须在返回时返回指定范围的文本,并用给定字符串替换一系列文本。 我们的示例中的类EditableCoreTextViewSimpleCoreTextView维护了支持字符串对象的同步副本(EditableCoreTextView作为NSMutableString对象)。Listing 9-6中的textInRange:和replaceRange:withText:的实现在后备字符串上调用适当的NSString方法来完成它们的基本功能。

// Listing 9-6  Implementations of textInRange: and replaceRange:withText:

- (NSString *)textInRange:(UITextRange *)range
{
    IndexedRange *r = (IndexedRange *)range;
    return ([_text substringWithRange:r.range]);
}
 
- (void)replaceRange:(UITextRange *)range withText:(NSString *)text
{
    IndexedRange *r = (IndexedRange *)range;
    NSRange selectedNSRange = _textView.selectedTextRange;
    if ((r.range.location + r.range.length) <= selectedNSRange.location) {
        selectedNSRange.location -= (r.range.length - text.length);
    } else {
        // Need to also deal with overlapping ranges.
    }
    [_text replaceCharactersInRange:r.range withString:text];
    _textView.text = _text;
    _textView.selectedTextRange = selectedNSRange;
}

SimpleCoreTextViewtext属性发生更改时(如replaceRange:withText :)的实现中所示,SimpleCoreTextView再次布局文本并使用Core Text函数重绘它。

Maintaining Selected and Marked Text Ranges - 维护选定和标记的文本范围

由于对选定和标记的文本执行编辑操作,因此文本输入系统经常请求返回文本视图并设置所选文本和标记文本的范围。 Listing 9-7显示了EditableCoreTextView如何通过为selectedTextRange和markedTextRange属性实现getter方法来返回所选文本和标记文本的范围。

// Listing 9-7  Returning ranges of selected and marked text

- (UITextRange *)selectedTextRange {
    return [IndexedRange rangeWithNSRange:_textView.selectedTextRange];
}
 
- (UITextRange *)markedTextRange {
    return [IndexedRange rangeWithNSRange:_textView.markedTextRange];
}

Listing 9-8selectedTextRange的setter方法只是在嵌入的文本视图中设置选定的文本范围。 setMarkedText:selectedRange:方法更复杂,因为您可能还记得,标记文本的范围包含所选文本的范围(即使范围仅标识插入符号),并且必须协调这些范围以反映插入文字后的情况。

// Listing 9-8  Setting the range of selected text and setting the marked text

- (void)setSelectedTextRange:(UITextRange *)range
{
    IndexedRange *r = (IndexedRange *)range;
    _textView.selectedTextRange = r.range;
}
 
- (void)setMarkedText:(NSString *)markedText selectedRange:(NSRange)selectedRange {
    NSRange selectedNSRange = _textView.selectedTextRange;
    NSRange markedTextRange = _textView.markedTextRange;
 
    if (markedTextRange.location != NSNotFound) {
        if (!markedText)
            markedText = @"";
        [_text replaceCharactersInRange:markedTextRange withString:markedText];
        markedTextRange.length = markedText.length;
    } else if (selectedNSRange.length > 0) {
        [_text replaceCharactersInRange:selectedNSRange withString:markedText];
        markedTextRange.location = selectedNSRange.location;
        markedTextRange.length = markedText.length;
    } else {
        [_text insertString:markedText atIndex:selectedNSRange.location];
        markedTextRange.location = selectedNSRange.location;
        markedTextRange.length = markedText.length;
    }
    selectedNSRange = NSMakeRange(selectedRange.location + markedTextRange.location,
        selectedRange.length);
 
    _textView.text = _text;
    _textView.markedTextRange = markedTextRange;
    _textView.selectedTextRange = selectedNSRange;
}

请注意,EditableCoreTextView通过在其可变字符串对象上调用replaceCharactersInRange:withString:方法来替换文本,然后将其分配给嵌入文本视图的text属性。

Frequently Called UITextInput Methods - 经常调用的UITextInput方法

当用户在键盘上键入字符并且当这些字符进入文本存储并被布局时,文本输入系统从采用UITextInput协议的对象请求信息。 三种更频繁调用的方法是textRangeFromPosition:toPosition:, offsetFromPosition:toPosition: 和 positionFromPosition:offset:。

文本输入系统调用positionFromPosition:offset:来获取文本中与另一个位置给定偏移量的位置。 Listing 9-9显示了EditableCoreTextView如何实现此方法(包括范围检查)。

// Listing 9-9  Implementing positionFromPosition:offset:

- (UITextPosition *)positionFromPosition:(UITextPosition *)position offset:(NSInteger)offset {
    IndexedPosition *pos = (IndexedPosition *)position;
    NSInteger end = pos.index + offset;
    if (end > _text.length || end < 0)
        return nil;
    return [IndexedPosition positionWithIndex:end];
}

offsetFromPosition:toPosition:方法应满足相反的请求并返回指定两个文本位置之间的偏移量的值。 EditableCoreTextView实现它,如Listing 9-10所示。

// Listing 9-10  Implementing offsetFromPosition:toPosition:

- (NSInteger)offsetFromPosition:(UITextPosition *)from toPosition:(UITextPosition *)toPosition {
    IndexedPosition *f = (IndexedPosition *)from;
    IndexedPosition *t = (IndexedPosition *)toPosition;
    return (t.index - f.index);
}

最后,文本输入系统经常向文本视图询问落在两个文本位置之间的文本范围。 Listing 9-11显示了textRangeFromPosition:toPosition:返回此范围方法的实现。

// Listing 9-11  Implementing textRangeFromPosition:toPosition:

- (UITextRange *)textRangeFromPosition:(UITextPosition *)fromPosition
          toPosition:(UITextPosition *)toPosition {
    IndexedPosition *from = (IndexedPosition *)fromPosition;
    IndexedPosition *to = (IndexedPosition *)toPosition;
    NSRange range = NSMakeRange(MIN(from.index, to.index), ABS(to.index - from.index));
    return [IndexedRange rangeWithNSRange:range];
}

Returning Rectangles - 返回矩形

当出现修正气泡并且用户输入日语时,文本输入系统会将firstRectForRange:和caretRectForPosition:发送到文本视图。 这两种方法的目的是返回一个矩形,该矩形包含一系列文本或标记插入点的插入符号。 EditableCoreTextView类通过调用嵌入式文本视图的方法来实现这些方法中的第一个,该方法将范围映射到一个封闭的矩形(参见Listing 9-12)。 在返回矩形之前,它将其转换为本地坐标系。

// Listing 9-12  An implementation of firstRectForRange:

- (CGRect)firstRectForRange:(UITextRange *)range {
    IndexedRange *r = (IndexedRange *)range;
    CGRect rect = [_textView firstRectForNSRange:r.range];
    return [self convertRect:rect fromView:_textView];
}

在这种情况下,嵌入式文本视图执行大部分工作。 使用Core Text函数,它计算包含文本范围的矩形并返回它,如Listing 9-13所示。

// Listing 9-13  Mapping text range to enclosing rectangle

- (CGRect)firstRectForNSRange:(NSRange)range; {
    int index = range.location;
    NSArray *lines = (NSArray *) CTFrameGetLines(_frame);
    for (int i = 0; i < [lines count]; i++) {
        CTLineRef line = (CTLineRef) [lines objectAtIndex:i];
        CFRange lineRange = CTLineGetStringRange(line);
        int localIndex = index - lineRange.location;
        if (localIndex >= 0 && localIndex < lineRange.length) {
            int finalIndex = MIN(lineRange.location + lineRange.length,
                range.location + range.length);
            CGFloat xStart = CTLineGetOffsetForStringIndex(line, index, NULL);
            CGFloat xEnd = CTLineGetOffsetForStringIndex(line, finalIndex, NULL);
            CGPoint origin;
            CTFrameGetLineOrigins(_frame, CFRangeMake(i, 0), &origin);
            CGFloat ascent, descent;
            CTLineGetTypographicBounds(line, &ascent, &descent, NULL);
 
            return CGRectMake(xStart, origin.y - descent, xEnd - xStart, ascent + descent);
        }
    }
    return CGRectNull;
}

对于caretRectForPosition:,你采取的方法会有所不同。 选择亲和力(selectionAffinity)是需要考虑的因素;更重要的是,请记住,插入符矩形的高度和宽度可以与firstRectForRange:返回的边界矩形不同。

Hit Testing - 命中测试

文本输入系统要求文本视图在文本显示和文本存储之间进行映射的另一个区域是命中测试。 给定文本视图中的一个点(文本输入系统要求),相应的文本位置或文本范围是什么? 它调用此信息的UITextInput方法是closestPositionToPoint:, closestPositionToPoint:withinRange: 和 characterRangeAtPoint:。 Listing 9-14说明了EditableCoreTextView如何实现第一个这些方法。

// Listing 9-14  An implementation of closestPositionToPoint:

- (UITextPosition *)closestPositionToPoint:(CGPoint)point {
    NSInteger index = [_textView closestIndexToPoint:point];
    return [IndexedPosition positionWithIndex:(NSUInteger)index];
}

这里,与返回文本范围或文本位置的矩形的方法一样,EditableCoreTextView调用其嵌入视图的方法,该方法使用Core Text计算与该点对应的字符索引。 Listing 9-15说明了嵌入式视图如何实现这一点。

// Listing 9-15  Mapping a point to a character index

- (NSInteger)closestIndexToPoint:(CGPoint)point {
    NSArray *lines = (NSArray *) CTFrameGetLines(_frame);
    CGPoint origins[lines.count];
    CTFrameGetLineOrigins(_frame, CFRangeMake(0, lines.count), origins);
 
    for (int i = 0; i < lines.count; i++) {
        if (point.y > origins[i].y) {
            CTLineRef line = (CTLineRef) [lines objectAtIndex:i];
            return CTLineGetStringIndexForPosition(line, point);
        }
    }
    return  _text.length;
}

Informing the Text Input Delegate of Changes - 通知文本输入变更代理

如果文本更改或选择更改未由文本输入系统启动,则应通过向其发送适当的will-change方法通知文本输入代理。进行更改后,向代理发送相应的“did-change”方法。

文本输入代理是系统提供的对象,它采用UITextInputDelegate协议。如果采用UITextInput协议的类定义了inputDelegate属性,则文本输入系统会在运行时自动将代理对象分配给此属性。

Listing 9-16显示了当用户点击文本视图时调用的操作方法。如果点击了视图但它不是第一个响应者,则文本视图使自己成为第一个响应者并开始编辑会话。如果随后点击视图,则文本视图会向文本输入代理发送selectionWillChange:消息。然后,它会清除任何标记的文本范围,并重置所选的文本范围,以使插入符号位于文本中出现轻敲的位置。在此之后,它调用selectionDidChange:。

// Listing 9-16  Sending messages to the text input delegate

- (void)tap:(UITapGestureRecognizer *)tap
{
    if (![self isFirstResponder]) {
        _textView.editing = YES;
        [self becomeFirstResponder];
    } else {
        [self.inputDelegate selectionWillChange:self];
 
        NSInteger index = [_textView closestIndexToPoint:[tap locationInView:_textView]];
        _textView.markedTextRange = NSMakeRange(NSNotFound, 0);
        _textView.selectedTextRange = NSMakeRange(index, 0);
 
        [self.inputDelegate selectionDidChange:self];
    }
}

Spell Checking and Word Completion - 拼写检查和单词完成

使用UITextChecker类的实例,您可以检查文档的拼写或提供完成部分输入的单词的建议。拼写检查文档时,UITextChecker对象搜索指定偏移量的文档。当它检测到拼写错误的单词时,它还可以返回一组可能正确的拼写,按照应该呈现给用户的顺序排列(即,最可能的替换单词首先出现)。您通常在每个文档中使用单个UITextChecker实例,但如果要共享忽略的单词和其他状态,则可以使用单个实例拼写检查相关的文本。

注意:UITextChecker类用于拼写检查,而不是用于自动更正。自动更正是您的文本文档可以通过采用Communicating with the Text Input System中描述的子类来获取的功能。

用于检查文档拼写错误单词的方法是rangeOfMisspelledWordInString:range:startingAt:wrap:language:;用于获取可能替换单词列表的方法是guessesForWordRange:inString:language:。您按给定顺序调用这些方法。要检查整个文档,可以在循环中调用这两个方法,在循环的每个循环中将起始偏移重置为更正后的字后面的字符,如Listing 9-17所示。

// Listing 9-17  Spell-checking a document

- (IBAction)spellCheckDocument:(id)sender {
    NSInteger currentOffset = 0;
    NSRange currentRange = NSMakeRange(0, 0);
    NSString *theText = textView.text;
    NSRange stringRange = NSMakeRange(0, theText.length-1);
    NSArray *guesses;
    BOOL done = NO;
 
    NSString *theLanguage = [[UITextChecker availableLanguages] objectAtIndex:0];
    if (!theLanguage)
        theLanguage = @"en_US";
 
    while (!done) {
        currentRange = [textChecker rangeOfMisspelledWordInString:theText range:stringRange
            startingAt:currentOffset wrap:NO language:theLanguage];
        if (currentRange.location == NSNotFound) {
            done = YES;
            continue;
        }
        guesses = [textChecker guessesForWordRange:currentRange inString:theText
            language:theLanguage];
        NSLog(@"---------------------------------------------");
        NSLog(@"Word misspelled is %@", [theText substringWithRange:currentRange]);
        NSLog(@"Possible replacements are %@", guesses);
        NSLog(@" ");
        currentOffset = currentOffset + (currentRange.length-1);
    }
}

UITextChecker类包括告诉文本检查器忽略或学习单词的方法。如Listing 9-17中的方法所示,您应该显示一些用户界面,允许用户选择正确的拼写,告诉文本检查器忽略或学习单词,然后继续执行,而不是仅记录拼写错误的单词及其可能的替换。下一个单词没有做任何改动。 iPad应用程序的一种可能方法是使用弹出视图列出table view中的猜测,并包括替换,学习,忽略等按钮。

您还可以使用UITextChecker获取部分输入单词的完成,并在弹出视图中的table view中显示完成。对于此任务,您调用completionsForPartialWordRange:inString:language:方法,传入给定字符串中的范围以进行检查。此方法返回一个完成部分输入单词的可能单词数组。Listing 9-18显示了如何调用此方法并显示一个table view,其中列出了弹出视图中的完成。

// Listing 9-18  Presenting a list of word completions for the current partial string

- (IBAction)completeCurrentWord:(id)sender {
 
    self.completionRange = [self computeCompletionRange];
    // The UITextChecker object is cached in an instance variable
    NSArray *possibleCompletions = [textChecker completionsForPartialWordRange:self.completionRange
        inString:self.textStore language:@"en"];
 
    CGSize popOverSize = CGSizeMake(150.0, 400.0);
    completionList = [[CompletionListController alloc] initWithStyle:UITableViewStylePlain];
    completionList.resultsList = possibleCompletions;
    completionListPopover = [[UIPopoverController alloc] initWithContentViewController:completionList];
    completionListPopover.popoverContentSize = popOverSize;
    completionListPopover.delegate = self;
    // rectForPartialWordRange: is a custom method
    CGRect pRect = [self rectForPartialWordRange:self.completionRange];
    [completionListPopover presentPopoverFromRect:pRect inView:self
        permittedArrowDirections:UIPopoverArrowDirectionAny animated:YES];
}

后记

本篇主要讲述了更底层的文字处理技术,感兴趣的给个赞或者关注~~~

TextKit框架详细解析 (十四) —— 文本编程指南之更底层的文字处理技术 (十)_第3张图片

你可能感兴趣的:(TextKit框架详细解析 (十四) —— 文本编程指南之更底层的文字处理技术 (十))