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

版本記錄

版本號 時間
V1.0 2018.08.27

前言

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

開始

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

Swift 4, iOS 11, Xcode 9

Core Text是一個低級底層文本引擎盆偿,與Core Graphics / Quartz框架一起使用時柒爸,可以對布局和格式進行細粒度控制。

在iOS 7中事扭,Apple發(fā)布了一個名為Text Kit的高級庫捎稚,它存儲,布局和顯示具有各種排版特征的文本。盡管Text Kit功能強大且通常在布局文本時足夠今野,但Core Text可以提供更多控制葡公。例如,如果您需要直接使用Quartz条霜,請使用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)建一個非常簡單的雜志應(yīng)用程序蒲凶!

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

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

接下來矛纹,將Core Text框架添加到項目中:

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

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


Adding a Core Text View - 添加一個Core Text視圖

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

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

打開CTView.swift权她,并在導(dǎo)入UIKit下添加以下內(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虏肾。 要子類化主視圖控制器的視圖廓啊,請在“Class”字段中鍵入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(_ :)將自動運行以渲染視圖的背景層第步。
  • 2)展開您將用于繪圖的當(dāng)前圖形上下文疮装。
  • 3)創(chuàng)建一個限制繪圖區(qū)域的路徑,在這種情況下是整個視圖的邊界
  • 4)在Core Text中雌续,使用NSAttributedString(而不是String或NSString)來保存文本及其屬性。 將“Hello World”初始化為屬性字符串胯杭。
  • 5)CTFramesetterCreateWithAttributedString使用提供的屬性字符串創(chuàng)建CTFramesetter驯杜。 CTFramesetter將管理您的字體引用和繪圖框架。
  • 6)通過讓CTFramesetterCreateFrame呈現(xiàn)path中的整個字符串來創(chuàng)建CTFrame做个。
  • 7)CTFrameDraw在給定的上下文中繪制CTFrame鸽心。

這就是你需要繪制一些簡單的文字! Build居暖,運行并查看結(jié)果顽频。

呃哦......那似乎不對,是嗎太闺? 與許多低級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)

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

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

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


The Core Text Object Model - Core Text 對象模型

如果你對CTFramesetterCTFrame感到有點困惑 - 這沒關(guān)系策治,因為是時候說明一下了。

以下是Core Text對象模型的樣子:

創(chuàng)建CTFramesetter引用并為其提供NSAttributedString時兰吟,會自動為您創(chuàng)建CTTypesetter實例以管理您的字體通惫。 接下來,使用CTFramesetter創(chuàng)建一個或多個frames混蔼,您將在其中呈現(xiàn)文本讽膏。

創(chuàng)建frame時,為其提供要在其矩形內(nèi)渲染的文本子范圍拄丰。 Core Text會自動為每行文本創(chuàng)建一個CTLine府树,并為具有相同格式的每段文本創(chuàng)建一個CTRun。 例如料按,如果您將多個單詞連續(xù)顯示為紅色奄侠,則會創(chuàng)建一個CTRun,然后是另一個CTRun用于以下純文本载矿,然后是另一個用于粗體句子的CTRun等垄潮。Core Text根據(jù)提供的NSAttributedString屬性為您創(chuàng)建CTRuns烹卒。 此外,這些CTRun對象中的每一個都可以采用不同的屬性弯洗,因此您可以很好地控制字距旅急,連字,寬度牡整,高度等藐吮。


Onto the Magazine App!

下載并取消歸檔 the zombie magazine materials

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

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

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

首先杨拐,快速瀏覽一下zombies.txt和敬。 看看它在整個文本中如何包含括號格式標(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)建了一個變量來保存parseMarkup(_ :)生成的屬性字符串; 并創(chuàng)建了一個數(shù)組舱痘,最終將保存字典信息,定義文本中找到的圖像的大小离赫,位置和文件名芭逝。

編寫解析器通常很難,但是本教程的解析器將非常簡單并且僅支持打開標(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)這個正則表達式匹配文本塊與標(biāo)簽緊隨其后。 它表示切厘,“通過字符串查找萨咳,直到找到一個開口括號,然后查看字符串疫稿,直到你敲到一個右括號(或文檔的末尾)培他【榱剑”
  • 3)搜索標(biāo)記的整個范圍以進行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時螃壤,會有很多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代碼單元集合中的位置;然后將每個String.UTF16View.Index轉(zhuǎn)換為String.Index格式日麸;組合時寄啼,產(chǎn)生Swift的范圍格式:Range。 只要索引有效代箭,該方法將返回原始NSRange的Range表示墩划。

下面是時候回到處理文本和標(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”,相對于設(shè)備屏幕的大小晨缴。 如果fontName未生成有效的UIFont译秦,請將font設(shè)置為默認(rèn)字體。
  • 5)創(chuàng)建字體格式的字典击碗,將其應(yīng)用于parts [0]以創(chuàng)建屬性字符串筑悴,然后將該字符串附加到結(jié)果字符串。

要處理“font”標(biāo)記稍途,請在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)如果少于兩個部分雷猪,則跳過循環(huán)體的其余部分。否則晰房,將第二部分存儲為tag求摇。
  • 2)如果tag“font”開頭射沟,則創(chuàng)建一個正則表達式以查找字體的“ color”值,然后使用該正則表達式來枚舉tag的匹配“ color”值与境。在這種情況下验夯,應(yīng)該只有一個匹配的顏色值。
  • 3)如果enumerateMatches(in:options:range:using :)返回tag中有效范圍的有效match摔刁,請找到指示的值(例如<font color =“red”>返回“red”)并將“Color”附加到表單中一個UIColor選擇器挥转。執(zhí)行該選擇器,然后將類的顏色設(shè)置為返回的顏色(如果存在)共屈,否則設(shè)置為黑色绑谣。
  • 4)同樣,創(chuàng)建一個正則表達式來處理字體的“face”值拗引。如果找到匹配項借宵,請將fontName設(shè)置為該字符串。

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

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

實際上欲间,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)建了一個實例變量來保存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 _ {
}

讓我們一步一步的細分下惹骂。

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

Build并運行應(yīng)用程序装蓬!

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

后記

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市暗赶,隨后出現(xiàn)的幾起案子鄙币,更是在濱河造成了極大的恐慌肃叶,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件十嘿,死亡現(xiàn)場離奇詭異因惭,居然都是意外死亡,警方通過查閱死者的電腦和手機绩衷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門蹦魔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人咳燕,你說我怎么就攤上這事勿决。” “怎么了招盲?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵低缩,是天一觀的道長。 經(jīng)常有香客問我宪肖,道長表制,這世上最難降的妖魔是什么健爬? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任控乾,我火速辦了婚禮,結(jié)果婚禮上娜遵,老公的妹妹穿的比我還像新娘蜕衡。我一直安慰自己,他們只是感情好设拟,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布慨仿。 她就那樣靜靜地躺著,像睡著了一般纳胧。 火紅的嫁衣襯著肌膚如雪镰吆。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天跑慕,我揣著相機與錄音万皿,去河邊找鬼。 笑死核行,一個胖子當(dāng)著我的面吹牛牢硅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播芝雪,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼减余,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了惩系?” 一聲冷哼從身側(cè)響起位岔,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤如筛,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后抒抬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體妙黍,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年瞧剖,在試婚紗的時候發(fā)現(xiàn)自己被綠了拭嫁。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡抓于,死狀恐怖做粤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捉撮,我是刑警寧澤怕品,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站巾遭,受9級特大地震影響肉康,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜灼舍,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一吼和、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧骑素,春花似錦炫乓、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至创橄,卻和暖如春箩做,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背妥畏。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工邦邦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人咖熟。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓圃酵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親馍管。 傳聞我的和親對象是個殘疾皇子郭赐,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容