翻譯 - Core Text Tutorial for iOS: Making a Magazine App

原文:Core Text Tutorial for iOS: Making a Magazine App

更新說明:本教程已由Lyndsey Scott更新到Swift 4和Xcode 9赡矢。最初的教程是由Marin Todorov編寫的。

Core Text是一個low-level 文本引擎期揪,當(dāng)與Core Graphics/Quartz框架一起使用時,它可以讓你對布局和格式進行粒度更加精細的控制。

在iOS 7中坐搔,蘋果發(fā)布了一個名為Text Kit的高級庫,用于存儲、布局和顯示具有各種排版特征的文本逮光。雖然Text Kit是強大的,通常在布局文本時是足夠的辕录,但Core Text可以提供更多的控制睦霎。例如梢卸,如果你需要直接使用Quartz走诞,使用Core Text噪猾。如果你需要創(chuàng)建自己的布局引擎虚缎,Core Text將幫助你生成“ glyphs,以及它們彼此精細排版后相對定位彰触〈鞫福”

本教程將帶你通過使用Core Text創(chuàng)建一個非常簡單的雜志應(yīng)用程序的過程……

哦塞绿,《僵尸月刊》的讀者們已經(jīng)同意,只要你在本教程中還在使用它們恤批,他們就不會吃你的大腦……所以你可能想要盡快開始!

注意:要充分利用本教程异吻,你需要首先了解iOS開發(fā)的基礎(chǔ)知識。如果你是iOS開發(fā)新手喜庞,你應(yīng)該先看看這個網(wǎng)站上的其他教程诀浪。

開始

打開Xcode,用Single View Application Template創(chuàng)建一個新的Swift通用項目延都,命名為CoreTextMagazine雷猪。

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

在項目導(dǎo)航器中單擊項目文件(左側(cè)的條帶)
在“常規(guī)”下晰房,向下滾動到底部的“鏈接框架和庫”
點擊“+”搜索“CoreText”
選擇“CoreText.framework”求摇,點擊“Add”按鈕射沟。就是這樣!
項目設(shè)置已經(jīng)完成,該開始敲代碼了与境。

添加一個Core Text View

對于初學(xué)者來說验夯,你將創(chuàng)建一個自定義的UIView,它將在draw(_:)方法中使用Core Text嚷辅。

創(chuàng)建一個新的Cocoa Touch 類文件簿姨,繼承與UIView,類名CTView扁位。

打開CTView.swift趁俊,并在import UIKit下添加以下內(nèi)容:

import CoreText

接下來,將這個新的自定義視圖設(shè)置為應(yīng)用程序中的主視圖寺擂。打開Main.storyboard,打開右手邊的Utilities菜單垦细,然后在它的頂部工具欄中選擇Identity Inspector圖標(biāo)。在Interface Builder的左側(cè)菜單中挡逼,選擇View。工具菜單的Class字段現(xiàn)在應(yīng)該顯示UIView家坎。要子類化主視圖控制器的視圖虱疏,在Class字段中鍵入CTView并按Enter鍵做瞪。


image

接下來装蓬,打開CTView.swift矛物,用下面的語句替換掉注釋掉的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)
}

讓我們一步一步來履羞。

在視圖創(chuàng)建時,draw(_:)將自動運行以渲染視圖的backing layer被环。
展開將用于繪圖的當(dāng)前圖形上下文筛欢。
創(chuàng)建一個路徑來限制繪制區(qū)域版姑,在這個例子中是整個視圖的邊界
在Core Text中剥险,你使用NSAttributedString宪肖,而不是String或NSString控乾,來保存文本和它的屬性蜕衡。初始化“Hello World”為帶屬性字符串衷咽。
CTFramesetterCreateWithAttributedString使用提供的帶屬性字符串創(chuàng)建一個CTFramesetter镶骗。CTFramesetter將管理你的字體參考和你的繪圖框架鼎姊。
通過使用CTFramesetterCreateFrame渲染路徑內(nèi)的整個字符串相寇,創(chuàng)建一個CTFrame唤衫。
CTFrameDraw在給定的上下文中繪制CTFrame佳励。
這就是繪制一些簡單文本所需要的全部內(nèi)容!構(gòu)建赃承、運行并查看結(jié)果瞧剖。


image

這似乎不對抓于,是嗎?像許多低級的api一樣捉撮,Core Text使用y -翻轉(zhuǎn)的坐標(biāo)系統(tǒng)呕缭。更糟糕的是恢总,內(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)

這段代碼通過將轉(zhuǎn)換應(yīng)用到視圖的上下文來翻轉(zhuǎn)內(nèi)容砂豌。

構(gòu)建并運行這個應(yīng)用程序阳距。不要擔(dān)心狀態(tài)欄重疊筐摘,稍后你將學(xué)習(xí)如何用邊距修復(fù)這個問題咖熟。


image

祝賀你的第一個核心文本應(yīng)用!僵尸們對你的進展很滿意馍管。

Core Text Object Model

如果你對CTFramesetter和CTFrame有點困惑确沸,那沒關(guān)系,因為是時候澄清一下了岭洲。:]

Core Text對象模型是這樣的:


image

當(dāng)你創(chuàng)建一個CTFramesetter引用并為它提供一個NSAttributedString時,一個CTTypesetter的實例就會被自動創(chuàng)建以供你管理字體告私。接下來驻粟,使用CTFramesetter創(chuàng)建一個或多個用來呈現(xiàn)文本的幀蜀撑。

當(dāng)您創(chuàng)建一個框架時酷麦,您為它提供要在其矩形內(nèi)呈現(xiàn)的文本子范圍沃饶。Core Text自動為每一行文本創(chuàng)建一個CTLine糊肤,為每一段具有相同格式的文本創(chuàng)建一個CTRun馆揉。例如把介,Core Text會創(chuàng)建一個CTRun,如果你在一行中有幾個紅色的單詞向臀,然后另一個CTRun用于下面的純文本券膀,然后另一個CTRun用于一個粗體句子芹彬,等等舒帮。Core Text為你創(chuàng)建基于提供的NSAttributedString屬性的CTRun玩郊。此外译红,每個CTRun對象都可以采用不同的屬性侦厚,因此您可以很好地控制字距刨沦、連接已卷、寬度侧蘸、高度等讳癌。

進入雜志應(yīng)用程序!

下載并解壓縮僵尸雜志材料逢艘。
把這個文件夾拖到你的Xcode項目中它改。當(dāng)出現(xiàn)提示時央拖,請確保選中了Copy items if neededCreate groups鲜戒。

要創(chuàng)建應(yīng)用程序遏餐,您需要對文本應(yīng)用各種屬性失都。您將創(chuàng)建一個簡單的文本標(biāo)記解析器嗅剖,它將使用標(biāo)記來設(shè)置雜志的格式黔攒。

創(chuàng)建一個新的CocoaTouch Class文件督惰,繼承NSObject赏胚,命名為MarkupParser觉阅。

首先典勇,快速瀏覽一下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(_:)產(chǎn)生的帶屬性字符串;并創(chuàng)建了一個數(shù)組,該數(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)簽進行匹配钉迷。它說,“遍歷字符串直到你找到一個開始的括號舰蟆,然后遍歷字符串直到你碰到一個結(jié)束的括號(或文檔的結(jié)尾)身害∷欤”
  3. 搜索正則表達式匹配的標(biāo)記的整個范圍涨颜,然后生成生成的NSTextCheckingResults的數(shù)組庭瑰。

注意:要了解更多關(guān)于正則表達式的知識弹灭,請查看NSRegularExpression教程。

現(xiàn)在您已經(jīng)將所有文本和格式化標(biāo)記解析為塊酒来,接下來將遍歷塊以構(gòu)建帶屬性字符串堰汉。

但是在那之前翘鸭,你注意到match(在:options:range:)是如何接受一個NSRange作為參數(shù)的嗎?當(dāng)你將nsregulareexpression函數(shù)應(yīng)用到你的標(biāo)記字符串時,會有很多NSRange到Range的轉(zhuǎn)換生蚁。斯威夫特是我們所有人的好朋友邦投,所以它應(yīng)該得到幫助屯援。

仍然在MarkupParser.swift中狞洋,在文件末尾添加以下擴展名:

// 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ù)將字符串的開始和結(jié)束索引轉(zhuǎn)換為 utf16view格式編碼怕午,即字符串的UTF-16代碼單元集合中的位置;然后轉(zhuǎn)換每個String.UTF16View堡距。索引的字符串羽戒。指數(shù)格式;當(dāng)合并時,生成Swift的range格式:range驶社。只要索引是有效的亡电,該方法將返回原始NSRange的Range表示。

你的Swift現(xiàn)在很冷靜』蛳剑現(xiàn)在回到處理文本和標(biāo)記塊的問題。


image

在parseMarkup(_:)中添加下面的let塊(在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. 循環(huán)塊瓣赂。
  2. 獲取當(dāng)前NSTextCheckingResult的range煌集,打開range <String。索引>卷拘,并繼續(xù)處理塊栗弟,只要它存在乍赫。
  3. 用“<”分隔塊。第一部分包含雜志文本改鲫,第二部分包含標(biāo)簽(如果存在的話)。
  4. 使用fontName創(chuàng)建一個字體讲弄,當(dāng)前默認(rèn)為“Arial”依痊,以及一個相對于設(shè)備屏幕的大小避除。如果fontName沒有產(chǎn)生一個有效的UIFont,設(shè)置字體為默認(rèn)字體胸嘁。
  5. 創(chuàng)建一個字體格式的字典瓶摆,將其應(yīng)用于部分[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)體的其余部分书斜。否則诬辈,將第二部分存儲為標(biāo)記。
  2. 如果標(biāo)簽以"font"開頭荐吉,創(chuàng)建一個正則表達式來查找字體的"color"值焙糟,然后使用該正則表達式來枚舉標(biāo)簽匹配的"color"值。在這種情況下样屠,應(yīng)該只有一個匹配的顏色值穿撮。
  3. 如果enumerateMatches(在:options:range:using:)返回一個有效的匹配與標(biāo)簽中的有效范圍,找到指定的值(例如<font color="red">返回"red")并添加" color "以形成一個UIColor選擇器痪欲。執(zhí)行該選擇器悦穿,然后將類的顏色設(shè)置為返回的顏色(如果存在),如果不存在則設(shè)置為黑色勤揩。
  4. 類似地咧党,創(chuàng)建一個正則表達式來處理字體的“face”值秘蛔。如果找到匹配陨亡,設(shè)置fontName為該字符串。
    偉大的工作!現(xiàn)在parseMarkup(_:)可以獲取標(biāo)記并為Core Text生成一個NSAttributedString深员。

是時候把你的應(yīng)用喂給僵尸了!我的意思是负蠕,在你的應(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)建了一個實例變量來保存帶屬性字符串芭概,并創(chuàng)建了一個方法來從應(yīng)用程序的其他地方設(shè)置它赛不。

接下來,打開ViewController.swift罢洲,并在viewDidLoad()中添加以下內(nèi)容:

// 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文件中加載文本到字符串中。
  2. 創(chuàng)建一個新的解析器惹苗,輸入文本殿较,然后將返回的帶屬性字符串傳遞給ViewController的CTView。
  3. 構(gòu)建并運行應(yīng)用程序!


    image

太棒了?多虧了大約50行解析桩蓉,你可以簡單地使用一個文本文件來保存雜志應(yīng)用程序的內(nèi)容淋纲。

雜志的基本布局

如果你認(rèn)為一本關(guān)于僵尸新聞的月刊可以放在一頁紙里,那你就大錯特錯了!幸運的是院究,Core Text在布局列時變得特別有用洽瞬,因為CTFrameGetVisibleStringRange可以告訴你有多少文本適合給定的框架玷或。意思是,你可以創(chuàng)建一個列片任,當(dāng)它滿了偏友,你可以創(chuàng)建另一個列,等等对供。

在這個應(yīng)用程序中位他,你必須打印專欄,然后是頁面产场,然后是整本雜志鹅髓,以免冒犯不死族,所以……是時候把你的CTView子類變成UIScrollView了京景。

打開CTView.swift窿冯,將CTView類改為:

class CTView: UIScrollView {

看到這一幕,僵尸?該應(yīng)用程序現(xiàn)在可以支持一個永恒的不死冒險!是的——只用一行,現(xiàn)在就可以滾動和分頁了确徙。

image

到目前為止醒串,您已經(jīng)在draw(_:)中創(chuàng)建了框架設(shè)置器和框架,但由于您將有許多格式不同的列鄙皇,所以最好創(chuàng)建單獨的列實例芜赌。

創(chuàng)建一個新的Cocoa Touch Class文件,命名為CTColumnView子類化UIView伴逸。

打開CTColumnView.swift缠沈,并添加以下入門代碼:

import UIKit
import CoreText

class CTColumnView: UIView {
  
  // MARK: - Properties
  var ctFrame: CTFrame!
  
  // MARK: - Initializers
  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)!
  }
  
  required init(frame: CGRect, ctframe: CTFrame) {
    super.init(frame: frame)
    self.ctFrame = ctframe
    backgroundColor = .white
  }
  
  // MARK: - Life Cycle
  override func draw(_ rect: CGRect) {
    guard let context = UIGraphicsGetCurrentContext() else { return }
      
    context.textMatrix = .identity
    context.translateBy(x: 0, y: bounds.size.height)
    context.scaleBy(x: 1.0, y: -1.0)
      
    CTFrameDraw(ctFrame, context)
  }
}

這段代碼呈現(xiàn)一個CTFrame,就像你在CTView中最初做的那樣错蝴。自定義初始化器init(frame:ctframe:)設(shè)置:

視圖的框架洲愤。
要繪制到上下文中的CTFrame。
視圖的背景色為白色顷锰。
接下來柬赐,創(chuàng)建一個名為CTSettings.swift的新swift文件,它將保存您的列設(shè)置馍惹。

將CTSettings.swift中的內(nèi)容替換為以下內(nèi)容:

import UIKit
import Foundation

class CTSettings {
  //1
  // MARK: - Properties
  let margin: CGFloat = 20
  var columnsPerPage: CGFloat!
  var pageRect: CGRect!
  var columnRect: CGRect!
  
  // MARK: - Initializers
  init() {
    //2
    columnsPerPage = UIDevice.current.userInterfaceIdiom == .phone ? 1 : 2
    //3
    pageRect = UIScreen.main.bounds.insetBy(dx: margin, dy: margin)
    //4
    columnRect = CGRect(x: 0,
                        y: 0,
                        width: pageRect.width / columnsPerPage,
                        height: pageRect.height).insetBy(dx: margin, dy: margin)
  }
}
  1. 屬性將決定頁邊距(本教程默認(rèn)為20);每頁列數(shù);每頁包含列的框架;以及每頁每欄的幀大小躺率。
  2. 由于該雜志同時面向iPhone和iPad,所以在iPad上顯示兩個欄目万矾,在iPhone上顯示一個欄目悼吱,所以欄目的數(shù)量適合不同的屏幕尺寸。
  3. 根據(jù)頁邊距的大小插入頁面的整個邊界良狈,以計算pag直立后添。
    將pag直立的寬度除以每頁的列數(shù),并在新框架中插入columnRect的邊距薪丁。
    打開遇西,CTView.swift馅精,替換整個內(nèi)容如下:
import UIKit
import CoreText

class CTView: UIScrollView {

  //1
  func buildFrames(withAttrString attrString: NSAttributedString,
                   andImages images: [[String: Any]]) {
    //3
    isPagingEnabled = true
    //4
    let framesetter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString)
    //4
    var pageView = UIView()
    var textPos = 0
    var columnIndex: CGFloat = 0
    var pageIndex: CGFloat = 0
    let settings = CTSettings()
    //5
    while textPos < attrString.length {
    }
  }
}
  1. buildFrames(withAttrString:andImages:)將創(chuàng)建CTColumnViews,然后將它們添加到滾動視圖粱檀。
  2. 啟用滾動視圖的分頁行為;因此洲敢,每當(dāng)用戶停止?jié)L動時,滾動視圖就會立即進入位置茄蚯,因此每次只顯示一個完整的頁面压彭。
  3. 框架設(shè)置器將創(chuàng)建每個列的帶屬性文本的CTFrame。
  4. UIView頁面視圖將作為每個頁面的列子視圖的容器;textPos將跟蹤下一個字符;columnIndex將跟蹤當(dāng)前列;pageIndex將跟蹤當(dāng)前頁面;“設(shè)置”可以讓你訪問應(yīng)用程序的頁邊距大小渗常、每頁的列數(shù)壮不、頁面框架和列框架設(shè)置。
  5. 你將循環(huán)遍歷attrString并一列一列地布局文本皱碘,直到當(dāng)前文本位置到達末尾询一。
    開始循環(huán)attrString。在while textPos < attrString.length {中添加.:
//1
if columnIndex.truncatingRemainder(dividingBy: settings.columnsPerPage) == 0 {
  columnIndex = 0
  pageView = UIView(frame: settings.pageRect.offsetBy(dx: pageIndex * bounds.width, dy: 0))
  addSubview(pageView)
  //2
  pageIndex += 1
}   
//3
let columnXOrigin = pageView.frame.size.width / settings.columnsPerPage
let columnOffset = columnIndex * columnXOrigin
let columnFrame = settings.columnRect.offsetBy(dx: columnOffset, dy: 0)

  1. 如果列索引除以每頁的列數(shù)等于0癌椿,從而表明該列是其頁面上的第一個列健蕊,則創(chuàng)建一個新的頁面視圖來保存這些列。要設(shè)置它的框架如失,取邊距設(shè)置绊诲。pag豎立和偏移其x原點的當(dāng)前頁面索引乘以屏幕的寬度;因此,在分頁滾動視圖中褪贵,每個雜志頁面都將在前一個頁面的右側(cè)。
  2. 增加pageIndex抗俄。
  3. 通過設(shè)置劃分頁面視圖的寬度脆丁。columnsPerPage獲取第一列的x原點;用這個原點乘以列索引得到列的偏移量;然后通過獲取標(biāo)準(zhǔn)的columnRect并通過columnOffset偏移其x原點來創(chuàng)建當(dāng)前列的框架。
    接下來动雹,添加以下columnFrame初始化:
//1   
let path = CGMutablePath()
path.addRect(CGRect(origin: .zero, size: columnFrame.size))
let ctframe = CTFramesetterCreateFrame(framesetter, CFRangeMake(textPos, 0), path, nil)
//2
let column = CTColumnView(frame: columnFrame, ctframe: ctframe)
pageView.addSubview(column)
//3
let frameRange = CTFrameGetVisibleStringRange(ctframe)
textPos += frameRange.length
//4
columnIndex += 1
  1. 創(chuàng)建一個CGMutablePath列的大小槽卫,然后從textPos開始,呈現(xiàn)一個新的CTFrame與盡可能多的文本胰蝠。
  2. 創(chuàng)建一個CTColumnView與CGRect columnFrame和CTFrame CTFrame然后添加列pageView歼培。
  3. 使用ctframegetvisiblestringgrange(_:)計算列中包含的文本范圍,然后將textPos遞增該范圍長度以反映當(dāng)前文本位置茸塞。
  4. 在循環(huán)到下一列之前躲庄,將列索引遞增1。
    最后钾虐,在循環(huán)之后設(shè)置滾動視圖的內(nèi)容大小:
contentSize = CGSize(width: CGFloat(pageIndex) * bounds.size.width,
                     height: bounds.size.height)

通過設(shè)置內(nèi)容大小為屏幕寬度乘以頁面數(shù)量噪窘,僵尸現(xiàn)在可以滾動到最后。

打開ViewController.swift效扫,并替換
(view as? CTView)?.importAttrString(parser.attrString)


(view as? CTView)?.buildFrames(withAttrString: parser.attrString, andImages: parser.images)

在iPad上構(gòu)建并運行應(yīng)用程序倔监。檢查雙列布局!左右拖動可以在頁面之間切換直砂。看起來似乎不錯 ??

image

您有列和格式化的文本浩习,但您缺少圖像静暂。用Core Text繪制圖像并不是那么簡單——畢竟它是一個文本框架——但是在你已經(jīng)創(chuàng)建的標(biāo)記解析器的幫助下,添加圖像應(yīng)該不會太糟糕谱秽。

在核心文本繪制圖像

雖然Core Text不能繪制圖像籍嘹,但作為一個布局引擎,它可以為圖像留出空間弯院。通過設(shè)置CTRun的委托辱士,可以確定CTRun的上升空間、下降空間和寬度听绳。像這樣:


image

當(dāng)Core Text到達一個帶有CTRunDelegate的CTRun時颂碘,它會問delegate,“我應(yīng)該為這個數(shù)據(jù)塊留下多少空間?”通過在CTRunDelegate中設(shè)置這些屬性椅挣,您可以在您的圖像的文本中留下空間头岔。

首先添加對“img”標(biāo)簽的支持。打開MarkupParser.swift鼠证,找到"} //end of font parsing".峡竣。在后面立即添加以下內(nèi)容:

//1
else if tag.hasPrefix("img") { 
      
  var filename:String = ""
  let imageRegex = try NSRegularExpression(pattern: "(?<=src=\")[^\"]+",
                                           options: NSRegularExpression.Options(rawValue: 0))
  imageRegex.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) {
        filename = String(tag[range])
    }
  }
  //2
  let settings = CTSettings()
  var width: CGFloat = settings.columnRect.width
  var height: CGFloat = 0

  if let image = UIImage(named: filename) {
    height = width * (image.size.height / image.size.width)
    // 3
    if height > settings.columnRect.height - font.lineHeight {
      height = settings.columnRect.height - font.lineHeight
      width = height * (image.size.width / image.size.height)
    }
  }
}
  1. 如果標(biāo)簽以"img"開頭,使用regex搜索圖像的"src"值量九,即文件名适掰。
  2. 將圖像寬度設(shè)置為列的寬度,并設(shè)置其高度荠列,以便圖像保持其高度-寬度寬高比类浪。
  3. 如果圖像的高度對于列來說太長,則設(shè)置高度以適應(yīng)列肌似,并減少寬度以保持圖像的長寬比费就。由于圖像后面的文本將包含空格屬性,所以包含空格信息的文本必須適合于圖像的同一列;因此將圖像高度設(shè)置為settings.columnRect.height - font.lineHeight川队。
    接下來力细,在if let圖像塊之后添加如下內(nèi)容:
//1
images += [["width": NSNumber(value: Float(width)),
            "height": NSNumber(value: Float(height)),
            "filename": filename,
            "location": NSNumber(value: attrString.length)]]
//2
struct RunStruct {
  let ascent: CGFloat
  let descent: CGFloat
  let width: CGFloat
}

let extentBuffer = UnsafeMutablePointer<RunStruct>.allocate(capacity: 1)
extentBuffer.initialize(to: RunStruct(ascent: height, descent: 0, width: width))
//3
var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { (pointer) in
}, getAscent: { (pointer) -> CGFloat in
  let d = pointer.assumingMemoryBound(to: RunStruct.self)
  return d.pointee.ascent
}, getDescent: { (pointer) -> CGFloat in
  let d = pointer.assumingMemoryBound(to: RunStruct.self)
  return d.pointee.descent
}, getWidth: { (pointer) -> CGFloat in
  let d = pointer.assumingMemoryBound(to: RunStruct.self)
  return d.pointee.width
})
//4
let delegate = CTRunDelegateCreate(&callbacks, extentBuffer)
//5
let attrDictionaryDelegate = [(kCTRunDelegateAttributeName as NSAttributedStringKey): (delegate as Any)]              
attrString.append(NSAttributedString(string: " ", attributes: attrDictionaryDelegate))
  1. 附加一個包含圖像大小,文件名和文本位置的Dictonary到圖像固额。
  2. 定義RunStruct來保存描述空白的屬性眠蚂。然后初始化一個指針,使其包含一個RunStruct对雪,其上升高度等于圖像高度河狐,寬度屬性等于圖像寬度。
  3. 創(chuàng)建一個CTRunDelegateCallbacks,返回屬于RunStruct類型指針的上升馋艺、下降和寬度屬性栅干。
  4. 使用ctrundelegateccreate創(chuàng)建一個綁定回調(diào)和數(shù)據(jù)參數(shù)的委托實例。
  5. 創(chuàng)建一個包含委托實例的帶屬性字典捐祠,然后向attrString追加一個空格碱鳞,它保存文本中空洞的位置和大小信息。

現(xiàn)在MarkupParser處理“img”標(biāo)簽踱蛀,你需要調(diào)整CTColumnView和CTView渲染他們窿给。

CTColumnView.swift開放。添加以下var ctFrame: ctFrame !保存柱子的圖像和框架:

var images: [(image: UIImage, frame: CGRect)] = []

接下來率拒,將以下內(nèi)容添加到draw(_:)的底部:

for imageData in images {
  if let image = imageData.image.cgImage {
    let imgBounds = imageData.frame
    context.draw(image, in: imgBounds)
  }
}

在這里崩泡,您可以遍歷每個圖像,并將其繪制到適當(dāng)框架的上下文中猬膨。

接下來打開CTView.swift和下面的屬性到類的頂部:

// MARK: - Properties
var imageIndex: Int!

imageIndex將在你繪制CTColumnViews時跟蹤當(dāng)前的圖像索引角撞。
接下來,添加以下內(nèi)容到buildFrames(withAttrString:andImages:)的頂部:

imageIndex = 0

這標(biāo)記了圖像數(shù)組的第一個元素勃痴。

接下來添加下面的attachImagesWithFrame(_:ctframe:margin:columnView)谒所,在buildFrames(withAttrString:andImages:)下面:

func attachImagesWithFrame(_ images: [[String: Any]],
                           ctframe: CTFrame,
                           margin: CGFloat,
                           columnView: CTColumnView) {
  //1
  let lines = CTFrameGetLines(ctframe) as NSArray
  //2
  var origins = [CGPoint](repeating: .zero, count: lines.count)
  CTFrameGetLineOrigins(ctframe, CFRangeMake(0, 0), &origins)
  //3
  var nextImage = images[imageIndex]
  guard var imgLocation = nextImage["location"] as? Int else {
    return
  }
  //4
  for lineIndex in 0..<lines.count {
    let line = lines[lineIndex] as! CTLine
    //5
    if let glyphRuns = CTLineGetGlyphRuns(line) as? [CTRun], 
      let imageFilename = nextImage["filename"] as? String, 
      let img = UIImage(named: imageFilename)  { 
        for run in glyphRuns {

        }
    }
  }
}
  1. 獲取CTFrame的CTLine對象的數(shù)組绝骚。
  2. 使用CTFrameGetOrigins將ctframe的行原點復(fù)制到origin數(shù)組中毕籽。通過設(shè)置一個長度為0的范圍娃惯,CTFrameGetOrigins將知道要遍歷整個CTFrame儒将。
  3. 設(shè)置nextImage以包含當(dāng)前圖像的屬性數(shù)據(jù)。如果nextImage包含圖像的位置碗暗,展開它并繼續(xù);否則,提前返回萍摊。
  4. 循環(huán)文本行倡勇。
  5. 如果該行的字形運行衫贬,filename和image with filename都存在德澈,則循環(huán)執(zhí)行該行的字形運行。
    接下來固惯,在符號run for-loop中添加以下內(nèi)容:
// 1
let runRange = CTRunGetStringRange(run)    
if runRange.location > imgLocation || runRange.location + runRange.length <= imgLocation {
  continue
}
//2
var imgBounds: CGRect = .zero
var ascent: CGFloat = 0       
imgBounds.size.width = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, nil, nil))
imgBounds.size.height = ascent
//3
let xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, nil)
imgBounds.origin.x = origins[lineIndex].x + xOffset 
imgBounds.origin.y = origins[lineIndex].y
//4
columnView.images += [(image: img, frame: imgBounds)]
//5
imageIndex! += 1
if imageIndex < images.count {
  nextImage = images[imageIndex]
  imgLocation = (nextImage["location"] as AnyObject).intValue
}
  1. 如果當(dāng)前運行的范圍不包含下一個圖像,則跳過循環(huán)的其余部分缴守。否則葬毫,在這里渲染圖像。
  2. 使用CTRunGetTypographicBounds計算圖像寬度屡穗,并將高度設(shè)置為找到的上升高度贴捡。
  3. 使用CTLineGetOffsetForStringIndex獲取直線的x偏移量,然后將其添加到imgBounds的原點村砂。
  4. 將圖像及其幀添加到當(dāng)前的CTColumnView中烂斋。
  5. 增加圖像索引。如果有一個圖像在images[imageIndex],更新nextImage和imgLocation汛骂,以便它們引用下一個圖像罕模。
image

好的!太棒了!差不多了,最后一步帘瞭。

添加以下右邊的pageView.addSubview(列)內(nèi)的buildFrames(withAttrString:and imagages:)附加圖像淑掌,如果他們存在:

if images.count > imageIndex {
  attachImagesWithFrame(images, ctframe: ctframe, margin: settings.margin, columnView: column)
}

看看iphone 和ipad的效果:


image

恭喜!為了感謝你們的辛勤工作,僵尸們放過了你們的大腦!:]

哪里可以看到效果?

在這里查看完成的項目蝶念。

正如在介紹中提到的抛腕,Text Kit通常可以替代Core Text;所以試著用Text Kit編寫這個教程媒殉,看看它是如何比較的担敌。也就是說,這堂 Core Text課不會是徒勞的!TextKit提供了免費的橋接到CoreText廷蓉,所以你有需要的話全封,可以很容易地進行框架之間的轉(zhuǎn)換。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末苦酱,一起剝皮案震驚了整個濱河市售貌,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌疫萤,老刑警劉巖颂跨,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異扯饶,居然都是意外死亡恒削,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門尾序,熙熙樓的掌柜王于貴愁眉苦臉地迎上來钓丰,“玉大人,你說我怎么就攤上這事每币⌒。” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵梦鉴,是天一觀的道長。 經(jīng)常有香客問我揭保,道長肥橙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任秸侣,我火速辦了婚禮存筏,結(jié)果婚禮上宠互,老公的妹妹穿的比我還像新娘。我一直安慰自己椭坚,他們只是感情好予跌,可當(dāng)我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著藕溅,像睡著了一般匕得。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上巾表,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天汁掠,我揣著相機與錄音,去河邊找鬼集币。 笑死考阱,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鞠苟。 我是一名探鬼主播乞榨,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼当娱!你這毒婦竟也來了吃既?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤跨细,失蹤者是張志新(化名)和其女友劉穎鹦倚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體冀惭,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡震叙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了散休。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片媒楼。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖戚丸,靈堂內(nèi)的尸體忽然破棺而出划址,到底是詐尸還是另有隱情,我是刑警寧澤限府,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布猴鲫,位于F島的核電站,受9級特大地震影響谣殊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜牺弄,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一姻几、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦蛇捌、人聲如沸抚恒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽俭驮。三九已至,卻和暖如春春贸,著一層夾襖步出監(jiān)牢的瞬間混萝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工萍恕, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留逸嘀,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓允粤,卻偏偏與公主長得像崭倘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子类垫,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,619評論 2 354

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