Core Text框架詳細(xì)解析(六) —— 基于Core Text的Magazine App的制作(一)

版本記錄

版本號(hào) 時(shí)間
V1.0 2018.08.27

前言

Core Text框架主要用來做文字處理,是的iOS3.2+OSX10.5+中的文本引擎,讓您精細(xì)的控制文本布局和格式。它位于在UIKit中和CoreGraphics/Quartz之間的最佳點(diǎn)。接下來這幾篇我們就主要解析該框架。感興趣的可以前面幾篇。
1. Core Text框架詳細(xì)解析(一) —— 基本概覽
2. Core Text框架詳細(xì)解析(二) —— 關(guān)于Core Text
3. Core Text框架詳細(xì)解析(三) —— Core Text總體概覽
4. Core Text框架詳細(xì)解析(四) —— Core Text文本布局操作
5. Core Text框架詳細(xì)解析(五) —— Core Text字體操作

開始

首先看一下本文的寫作環(huán)境:

Swift 4, iOS 11, Xcode 9

Core Text是一個(gè)低級(jí)底層文本引擎,與Core Graphics / Quartz框架一起使用時(shí),可以對(duì)布局和格式進(jìn)行細(xì)粒度控制。

在iOS 7中,Apple發(fā)布了一個(gè)名為Text Kit的高級(jí)庫,它存儲(chǔ),布局和顯示具有各種排版特征的文本。盡管Text Kit功能強(qiáng)大且通常在布局文本時(shí)足夠,但Core Text可以提供更多控制。例如,如果您需要直接使用Quartz,請(qǐng)使用Core Text。如果您需要構(gòu)建自己的布局引擎,Core Text將幫助您生成“glyphs and position them relative to each other with all the features of fine typesetting.”

本教程將指導(dǎo)您使用Core Text創(chuàng)建一個(gè)非常簡(jiǎn)單的雜志應(yīng)用程序!

下面就新建立一個(gè)工程并開始我們的工作。

打開Xcode,使用Single View Application Template創(chuàng)建一個(gè)新的Swift universal project,并將其命名為CoreTextMagazine。

接下來,將Core Text框架添加到項(xiàng)目中:

  • 1)單擊項(xiàng)目導(dǎo)航器中的項(xiàng)目文件(左側(cè)的條帶)
  • 2)在“General”下,向下滾動(dòng)到底部的“Linked Frameworks and Libraries”
  • 3)點(diǎn)擊“+”并搜索“CoreText”
  • 4)選擇“CoreText.framework”并單擊“Add”按鈕。 而已!

現(xiàn)在項(xiàng)目已經(jīng)設(shè)置好,是時(shí)候開始編碼了。


Adding a Core Text View - 添加一個(gè)Core Text視圖

對(duì)于初學(xué)者,您將創(chuàng)建一個(gè)自定義UIView,它將在其draw(_ :)方法中使用Core Text。

創(chuàng)建一個(gè)名為CTView繼承自UIView的新Cocoa Touch類文件。

打開CTView.swift,并在導(dǎo)入U(xiǎn)IKit下添加以下內(nèi)容:

import CoreText

接下來,將此新自定義視圖設(shè)置為應(yīng)用程序中的主視圖。 打開Main.storyboard,打開右側(cè)的Utilities菜單,然后在其頂部工具欄中選擇Identity Inspector圖標(biāo)。 在Interface Builder的左側(cè)菜單中,選擇View。 Utilities菜單的Class字段現(xiàn)在應(yīng)該是UIView。 要子類化主視圖控制器的視圖,請(qǐng)?jiān)凇癈lass”字段中鍵入CTView,然后按Enter鍵。

接下來,打開CTView.swift并用以下內(nèi)容替換注釋掉的draw(_:)

         
//1      
override func draw(_ rect: CGRect) {         
  // 2       
  guard let context = UIGraphicsGetCurrentContext() else { return }      
  // 3       
  let path = CGMutablePath()         
  path.addRect(bounds)       
  // 4
  let attrString = NSAttributedString(string: "Hello World")
  // 5
  let framesetter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString)
  // 6
  let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrString.length), path, nil) 
  // 7
  CTFrameDraw(frame, context)
}

下面,讓我們一步一步地回顧一下:

  • 1)創(chuàng)建視圖后,draw(_ :)將自動(dòng)運(yùn)行以渲染視圖的背景層。
  • 2)展開您將用于繪圖的當(dāng)前圖形上下文。
  • 3)創(chuàng)建一個(gè)限制繪圖區(qū)域的路徑,在這種情況下是整個(gè)視圖的邊界
  • 4)在Core Text中,使用NSAttributedString(而不是String或NSString)來保存文本及其屬性。 將“Hello World”初始化為屬性字符串。
  • 5)CTFramesetterCreateWithAttributedString使用提供的屬性字符串創(chuàng)建CTFramesetter。 CTFramesetter將管理您的字體引用和繪圖框架。
  • 6)通過讓CTFramesetterCreateFrame呈現(xiàn)path中的整個(gè)字符串來創(chuàng)建CTFrame。
  • 7)CTFrameDraw在給定的上下文中繪制CTFrame。

這就是你需要繪制一些簡(jiǎn)單的文字! Build,運(yùn)行并查看結(jié)果。

呃哦......那似乎不對(duì),是嗎? 與許多低級(jí)API一樣,Core Text使用Y-flipped坐標(biāo)系。 更糟糕的是,內(nèi)容也垂直翻轉(zhuǎn)!

guard let context語句的正下方添加以下代碼以修復(fù)內(nèi)容方向:

// Flip the coordinate system
context.textMatrix = .identity
context.translateBy(x: 0, y: bounds.size.height)
context.scaleBy(x: 1.0, y: -1.0)

此代碼通過對(duì)視圖的上下文應(yīng)用轉(zhuǎn)換來翻轉(zhuǎn)內(nèi)容。

Build并運(yùn)行應(yīng)用程序。 不要擔(dān)心狀態(tài)欄重疊,您將學(xué)習(xí)如何使用邊距修復(fù)此問題。

祝賀您的第一個(gè)Core Text應(yīng)用程序!


The Core Text Object Model - Core Text 對(duì)象模型

如果你對(duì)CTFramesetterCTFrame感到有點(diǎn)困惑 - 這沒關(guān)系,因?yàn)槭菚r(shí)候說明一下了。

以下是Core Text對(duì)象模型的樣子:

創(chuàng)建CTFramesetter引用并為其提供NSAttributedString時(shí),會(huì)自動(dòng)為您創(chuàng)建CTTypesetter實(shí)例以管理您的字體。 接下來,使用CTFramesetter創(chuàng)建一個(gè)或多個(gè)frames,您將在其中呈現(xiàn)文本。

創(chuàng)建frame時(shí),為其提供要在其矩形內(nèi)渲染的文本子范圍。 Core Text會(huì)自動(dòng)為每行文本創(chuàng)建一個(gè)CTLine,并為具有相同格式的每段文本創(chuàng)建一個(gè)CTRun。 例如,如果您將多個(gè)單詞連續(xù)顯示為紅色,則會(huì)創(chuàng)建一個(gè)CTRun,然后是另一個(gè)CTRun用于以下純文本,然后是另一個(gè)用于粗體句子的CTRun等。Core Text根據(jù)提供的NSAttributedString屬性為您創(chuàng)建CTRuns。 此外,這些CTRun對(duì)象中的每一個(gè)都可以采用不同的屬性,因此您可以很好地控制字距,連字,寬度,高度等。


Onto the Magazine App!

下載并取消歸檔 the zombie magazine materials。

將文件夾拖到Xcode項(xiàng)目中。 出現(xiàn)提示時(shí),確保選中Copy items if neededCreate groups

要?jiǎng)?chuàng)建應(yīng)用程序,您需要將各種屬性應(yīng)用于文本。 您將創(chuàng)建一個(gè)簡(jiǎn)單的文本標(biāo)記解析器,它將使用標(biāo)簽來設(shè)置雜志的格式。

創(chuàng)建一個(gè)名為MarkupParser的新Cocoa Touch類文件,繼承自NSObject。

首先,快速瀏覽一下zombies.txt。 看看它在整個(gè)文本中如何包含括號(hào)格式標(biāo)簽? “img src”標(biāo)簽引用雜志圖像和“font color / face”標(biāo)簽確定文本顏色和字體。

打開MarkupParser.swift并用以下內(nèi)容替換其內(nèi)容:

import UIKit
import CoreText

class MarkupParser: NSObject {
  
  // MARK: - Properties
  var color: UIColor = .black
  var fontName: String = "Arial"
  var attrString: NSMutableAttributedString!
  var images: [[String: Any]] = []

  // MARK: - Initializers
  override init() {
    super.init()
  }
  
  // MARK: - Internal
  func parseMarkup(_ markup: String) {

  }
}

在這里,您添加了保存字體和文本顏色的屬性;設(shè)置默認(rèn)值;創(chuàng)建了一個(gè)變量來保存parseMarkup(_ :)生成的屬性字符串; 并創(chuàng)建了一個(gè)數(shù)組,最終將保存字典信息,定義文本中找到的圖像的大小,位置和文件名。

編寫解析器通常很難,但是本教程的解析器將非常簡(jiǎn)單并且僅支持打開標(biāo)記 - 這意味著標(biāo)記將設(shè)置其后面的文本樣式,直到找到新標(biāo)記。 文本標(biāo)記將如下所示:

These are <font color="red">red<font color="black"> and
<font color="blue">blue <font color="black">words.

并產(chǎn)生這樣的輸出:

讓我們得到解析!

將以下內(nèi)容添加到parseMarkup(_ :)

//1
attrString = NSMutableAttributedString(string: "")
//2 
do {
  let regex = try NSRegularExpression(pattern: "(.*?)(<[^>]+>|\\Z)",
                                      options: [.caseInsensitive,
                                                .dotMatchesLineSeparators])
  //3
  let chunks = regex.matches(in: markup, 
                             options: NSRegularExpression.MatchingOptions(rawValue: 0), 
                             range: NSRange(location: 0,
                                            length: markup.characters.count))
} catch _ {
}
  • 1)attrString開始為空,但最終將包含已解析的標(biāo)記。
  • 2)這個(gè)正則表達(dá)式匹配文本塊與標(biāo)簽緊隨其后。 它表示,“通過字符串查找,直到找到一個(gè)開口括號(hào),然后查看字符串,直到你敲到一個(gè)右括號(hào)(或文檔的末尾)?!?/li>
  • 3)搜索標(biāo)記的整個(gè)范圍以進(jìn)行regex匹配,然后生成結(jié)果NSTextCheckingResult的數(shù)組。

現(xiàn)在,您已將所有文本和格式標(biāo)記解析為chunks,您將循環(huán)遍歷chunks以構(gòu)建屬性字符串。

但在此之前,您是否注意到matches(in:options:range:)如何接受NSRange作為參數(shù)? 將NSRegularExpression函數(shù)應(yīng)用于標(biāo)記String時(shí),會(huì)有很多NSRangeRange轉(zhuǎn)換。

仍然在MarkupParser.swift中,將以下extension添加到文件末尾:

// MARK: - String
extension String {
  func range(from range: NSRange) -> Range<String.Index>? {
    guard let from16 = utf16.index(utf16.startIndex,
                                   offsetBy: range.location,
                                   limitedBy: utf16.endIndex),
      let to16 = utf16.index(from16, offsetBy: range.length, limitedBy: utf16.endIndex),
      let from = String.Index(from16, within: self),
      let to = String.Index(to16, within: self) else {
        return nil
   }

    return from ..< to
  }
}

此函數(shù)將String的起始和結(jié)束索引(由NSRange表示)轉(zhuǎn)換為String.UTF16View.Index格式,即字符串的UTF-16代碼單元集合中的位置;然后將每個(gè)String.UTF16View.Index轉(zhuǎn)換為String.Index格式;組合時(shí),產(chǎn)生Swift的范圍格式:Range。 只要索引有效,該方法將返回原始NSRange的Range表示。

下面是時(shí)候回到處理文本和標(biāo)記塊了。

parseMarkup(_ :)里面添加以下內(nèi)容let chunks(在do塊中):

let defaultFont: UIFont = .systemFont(ofSize: UIScreen.main.bounds.size.height / 40)
//1
for chunk in chunks {  
  //2
  guard let markupRange = markup.range(from: chunk.range) else { continue }
  //3    
  let parts = markup[markupRange].components(separatedBy: "<")
  //4
  let font = UIFont(name: fontName, size: UIScreen.main.bounds.size.height / 40) ?? defaultFont       
  //5
  let attrs = [NSAttributedStringKey.foregroundColor: color, NSAttributedStringKey.font: font] as [NSAttributedStringKey : Any]
  let text = NSMutableAttributedString(string: parts[0], attributes: attrs)
  attrString.append(text)
}
  • 1)遍歷chunks。
  • 2)獲取當(dāng)前NSTextCheckingResult的范圍,打開Range <String.Index>并繼續(xù)塊,只要它存在。
  • 3)將chunk拆分為由“<”分隔的部分。 第一部分包含雜志文本,第二部分包含標(biāo)簽(如果存在)。
  • 4)使用fontName創(chuàng)建字體,默認(rèn)情況下為“Arial”,相對(duì)于設(shè)備屏幕的大小。 如果fontName未生成有效的UIFont,請(qǐng)將font設(shè)置為默認(rèn)字體。
  • 5)創(chuàng)建字體格式的字典,將其應(yīng)用于parts [0]以創(chuàng)建屬性字符串,然后將該字符串附加到結(jié)果字符串。

要處理“font”標(biāo)記,請(qǐng)?jiān)?code>attrString.append(text)之后插入以下內(nèi)容:

// 1
if parts.count <= 1 {
  continue
}
let tag = parts[1]
//2
if tag.hasPrefix("font") {
  let colorRegex = try NSRegularExpression(pattern: "(?<=color=\")\\w+", 
                                           options: NSRegularExpression.Options(rawValue: 0))
  colorRegex.enumerateMatches(in: tag, 
    options: NSRegularExpression.MatchingOptions(rawValue: 0), 
    range: NSMakeRange(0, tag.characters.count)) { (match, _, _) in
      //3
      if let match = match,
        let range = tag.range(from: match.range) {
          let colorSel = NSSelectorFromString(tag[range]+"Color")
          color = UIColor.perform(colorSel).takeRetainedValue() as? UIColor ?? .black
      }
  }
  //5    
  let faceRegex = try NSRegularExpression(pattern: "(?<=face=\")[^\"]+",
                                          options: NSRegularExpression.Options(rawValue: 0))
  faceRegex.enumerateMatches(in: tag, 
    options: NSRegularExpression.MatchingOptions(rawValue: 0), 
    range: NSMakeRange(0, tag.characters.count)) { (match, _, _) in

      if let match = match,
        let range = tag.range(from: match.range) {
          fontName = String(tag[range])
      }
  }
} //end of font parsing
  • 1)如果少于兩個(gè)部分,則跳過循環(huán)體的其余部分。否則,將第二部分存儲(chǔ)為tag
  • 2)如果tag“font”開頭,則創(chuàng)建一個(gè)正則表達(dá)式以查找字體的“ color”值,然后使用該正則表達(dá)式來枚舉tag的匹配“ color”值。在這種情況下,應(yīng)該只有一個(gè)匹配的顏色值。
  • 3)如果enumerateMatches(in:options:range:using :)返回tag中有效范圍的有效match,請(qǐng)找到指示的值(例如<font color =“red”>返回“red”)并將“Color”附加到表單中一個(gè)UIColor選擇器。執(zhí)行該選擇器,然后將類的顏色設(shè)置為返回的顏色(如果存在),否則設(shè)置為黑色。
  • 4)同樣,創(chuàng)建一個(gè)正則表達(dá)式來處理字體的“face”值。如果找到匹配項(xiàng),請(qǐng)將fontName設(shè)置為該字符串。

做得好!現(xiàn)在,parseMarkup(_ :)可以獲取標(biāo)記并為Core Text生成NSAttributedString

是時(shí)候給你的應(yīng)用程序提供zombies.txt了。

實(shí)際上,UIView的工作是顯示給定的內(nèi)容,而不是加載內(nèi)容。打開CTView.swift并在上面添加以下draw(_:)

// MARK: - Properties
var attrString: NSAttributedString!

// MARK: - Internal
func importAttrString(_ attrString: NSAttributedString) {
  self.attrString = attrString
}

接下來,從draw(_ :)中刪除let attrString = NSAttributedString(string:“Hello World”)。

在這里,您創(chuàng)建了一個(gè)實(shí)例變量來保存attributed string,以及一種從應(yīng)用程序的其他位置設(shè)置它的方法。

接下來,打開ViewController.swift并將以下內(nèi)容添加到viewDidLoad()

// 1
guard let file = Bundle.main.path(forResource: "zombies", ofType: "txt") else { return }
  
do {
  let text = try String(contentsOfFile: file, encoding: .utf8)
  // 2
  let parser = MarkupParser()
  parser.parseMarkup(text)
  (view as? CTView)?.importAttrString(parser.attrString)
} catch _ {
}

讓我們一步一步的細(xì)分下。

  • 1)將zombie.txt文件中的文本加載到String中。
  • 2)創(chuàng)建一個(gè)新的解析器,輸入文本,然后將返回的屬性字符串傳遞給ViewControllerCTView。

Build并運(yùn)行應(yīng)用程序!

棒極了? 感謝大約50行解析,您只需使用文本文件來保存雜志應(yīng)用程序的內(nèi)容。

后記

本篇主要講述了基于Core Text的Magazine App的制作,感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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