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 AnimationCATextLayer類的來進行簡單的文本繪制。此類的對象將純字符串或屬性字符串存儲為其內容,并提供一組影響該內容的屬性,如字體,字體大小,文本顏色和截斷行為。 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對象,但它會從字形運行數組合成這些行。

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)是字體描述符組,提供字體枚舉和對全局和自定義字體集合的訪問等服務。

可以通過調用CTFontCreateWithNameUIFont對象轉換為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方法如下:

如果在這些方法中指定NSRegularExpressionSearch選項,則可以指定的唯一其他NSStringCompareOptions選項是NSCaseInsensitiveSearchNSAnchoredSearch。 如果正則表達式搜索未找到匹配項或者正則表達式語法格式錯誤,則這些方法將返回值為{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所示。

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 - 文本位置和文本區間

客戶端應用程序必須創建兩個類,其實例表示文本視圖中文本的位置和范圍。這些類必須是UITextPositionUITextRange的子類。

雖然UITextPosition本身沒有聲明方法或屬性,但它是文本文檔和文本輸入系統之間交換信息的重要部分。文本輸入系統需要一個對象來表示文本中的位置,而不是整數或結構。此外,當支持文本的字符串與該位置具有不同的偏移量時,UITextPosition對象可以通過表示可見文本中的位置來實現實際目的。當字符串包含不可見的格式字符(例如RTF和HTML文檔)或嵌入的對象(例如附件)時,會發生這種情況。在查找可見字符的字符串偏移時,自定義UITextPosition類可以考慮這些不可見字符。在最簡單的情況下 - 沒有嵌入對象的純文本文檔 - 自定義UITextPosition對象可以封裝單個偏移量或索引整數。

UITextRange聲明一個簡單的接口,其中兩個屬性是開始和結束自定義UITextPosition對象。第三個屬性包含一個布爾值,指示范圍是否為空(即沒有長度)。

Tasks of a UITextInput Object - UITextInput對象的任務

需要采用UITextInput協議的類來實現協議的大部分方法和屬性。除了少數例外,這些方法將自定義UITextPositionUITextRange對象作為參數或返回其中一個對象。在運行時,文本系統調用這些方法,并且在幾乎所有情況下,都會期望返回一些對象或值。

文本視圖必須將文本位置分配給標記顯示文本開頭和結尾的屬性。此外,它還必須保持當前所選文本的范圍和當前標記文本的范圍(如果有)。標記文本是多級文本輸入的一部分,表示用戶尚未確認的臨時插入文本。它以獨特的方式設計。標記文本的范圍始終包含一系列選定文本,可能是一系列字符或插入符號。

UITextInput對象實現的方法可以分為不同的任務:

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 <UITextInputDelegate> _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:, deleteBackwardhasText方法,如Simple Text Input中所述。 由于EditableCoreTextView類采用UITextInput,因此在輸入和刪除文本時,它還必須維護選定和標記的文本范圍(即selectedTextRangemarkedTextRange屬性的當前值)。

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如何通過為selectedTextRangemarkedTextRange屬性實現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];
}

后記

本篇主要講述了更底層的文字處理技術,感興趣的給個贊或者關注~~~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,748評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,165評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,595評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,633評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,435評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,943評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,035評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,175評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,713評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,599評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,788評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,303評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,034評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,412評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,664評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,408評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,747評論 2 370

推薦閱讀更多精彩內容