原文: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鍵做瞪。
接下來装蓬,打開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é)果瞧剖。
這似乎不對抓于,是嗎?像許多低級的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ù)這個問題咖熟。
祝賀你的第一個核心文本應(yīng)用!僵尸們對你的進展很滿意馍管。
Core Text Object Model
如果你對CTFramesetter和CTFrame有點困惑确沸,那沒關(guān)系,因為是時候澄清一下了岭洲。:]
Core Text對象模型是這樣的:
當(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 needed 和 Create 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 _ {
}
- attrString一開始為空,但最終將包含解析后的標(biāo)記广恢。
- 這個正則表達式將文本塊與緊跟其后的標(biāo)簽進行匹配钉迷。它說,“遍歷字符串直到你找到一個開始的括號舰蟆,然后遍歷字符串直到你碰到一個結(jié)束的括號(或文檔的結(jié)尾)身害∷欤”
- 搜索正則表達式匹配的標(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)記塊的問題。
在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)
}
- 循環(huán)塊瓣赂。
- 獲取當(dāng)前NSTextCheckingResult的range煌集,打開range <String。索引>卷拘,并繼續(xù)處理塊栗弟,只要它存在乍赫。
- 用“<”分隔塊。第一部分包含雜志文本改鲫,第二部分包含標(biāo)簽(如果存在的話)。
- 使用fontName創(chuàng)建一個字體讲弄,當(dāng)前默認(rèn)為“Arial”依痊,以及一個相對于設(shè)備屏幕的大小避除。如果fontName沒有產(chǎn)生一個有效的UIFont,設(shè)置字體為默認(rèn)字體胸嘁。
- 創(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
- 如果少于兩個部分毫胜,則跳過循環(huán)體的其余部分书斜。否則诬辈,將第二部分存儲為標(biāo)記。
- 如果標(biāo)簽以"font"開頭荐吉,創(chuàng)建一個正則表達式來查找字體的"color"值焙糟,然后使用該正則表達式來枚舉標(biāo)簽匹配的"color"值。在這種情況下样屠,應(yīng)該只有一個匹配的顏色值穿撮。
- 如果enumerateMatches(在:options:range:using:)返回一個有效的匹配與標(biāo)簽中的有效范圍,找到指定的值(例如<font color="red">返回"red")并添加" color "以形成一個UIColor選擇器痪欲。執(zhí)行該選擇器悦穿,然后將類的顏色設(shè)置為返回的顏色(如果存在),如果不存在則設(shè)置為黑色勤揩。
- 類似地咧党,創(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 _ {
}
讓我們一步一步來踢故。
- 從zombie.txt文件中加載文本到字符串中。
- 創(chuàng)建一個新的解析器惹苗,輸入文本殿较,然后將返回的帶屬性字符串傳遞給ViewController的CTView。
-
構(gòu)建并運行應(yīng)用程序!
太棒了?多虧了大約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)在就可以滾動和分頁了确徙。
到目前為止醒串,您已經(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)
}
}
- 屬性將決定頁邊距(本教程默認(rèn)為20);每頁列數(shù);每頁包含列的框架;以及每頁每欄的幀大小躺率。
- 由于該雜志同時面向iPhone和iPad,所以在iPad上顯示兩個欄目万矾,在iPhone上顯示一個欄目悼吱,所以欄目的數(shù)量適合不同的屏幕尺寸。
- 根據(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 {
}
}
}
-
buildFrames(withAttrString:andImages:)
將創(chuàng)建CTColumnViews,然后將它們添加到滾動視圖粱檀。 - 啟用滾動視圖的分頁行為;因此洲敢,每當(dāng)用戶停止?jié)L動時,滾動視圖就會立即進入位置茄蚯,因此每次只顯示一個完整的頁面压彭。
- 框架設(shè)置器將創(chuàng)建每個列的帶屬性文本的CTFrame。
- UIView頁面視圖將作為每個頁面的列子視圖的容器;textPos將跟蹤下一個字符;columnIndex將跟蹤當(dāng)前列;pageIndex將跟蹤當(dāng)前頁面;“設(shè)置”可以讓你訪問應(yīng)用程序的頁邊距大小渗常、每頁的列數(shù)壮不、頁面框架和列框架設(shè)置。
- 你將循環(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)
- 如果列索引除以每頁的列數(shù)等于0癌椿,從而表明該列是其頁面上的第一個列健蕊,則創(chuàng)建一個新的頁面視圖來保存這些列。要設(shè)置它的框架如失,取邊距設(shè)置绊诲。pag豎立和偏移其x原點的當(dāng)前頁面索引乘以屏幕的寬度;因此,在分頁滾動視圖中褪贵,每個雜志頁面都將在前一個頁面的右側(cè)。
- 增加pageIndex抗俄。
- 通過設(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
- 創(chuàng)建一個CGMutablePath列的大小槽卫,然后從textPos開始,呈現(xiàn)一個新的CTFrame與盡可能多的文本胰蝠。
- 創(chuàng)建一個CTColumnView與CGRect columnFrame和CTFrame CTFrame然后添加列pageView歼培。
- 使用ctframegetvisiblestringgrange(_:)計算列中包含的文本范圍,然后將textPos遞增該范圍長度以反映當(dāng)前文本位置茸塞。
- 在循環(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)用程序倔监。檢查雙列布局!左右拖動可以在頁面之間切換直砂。看起來似乎不錯 ??
您有列和格式化的文本浩习,但您缺少圖像静暂。用Core Text繪制圖像并不是那么簡單——畢竟它是一個文本框架——但是在你已經(jīng)創(chuàng)建的標(biāo)記解析器的幫助下,添加圖像應(yīng)該不會太糟糕谱秽。
在核心文本繪制圖像
雖然Core Text不能繪制圖像籍嘹,但作為一個布局引擎,它可以為圖像留出空間弯院。通過設(shè)置CTRun的委托辱士,可以確定CTRun的上升空間、下降空間和寬度听绳。像這樣:
當(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)
}
}
}
- 如果標(biāo)簽以"img"開頭,使用regex搜索圖像的"src"值量九,即文件名适掰。
- 將圖像寬度設(shè)置為列的寬度,并設(shè)置其高度荠列,以便圖像保持其高度-寬度寬高比类浪。
- 如果圖像的高度對于列來說太長,則設(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))
- 附加一個包含圖像大小,文件名和文本位置的Dictonary到圖像固额。
- 定義RunStruct來保存描述空白的屬性眠蚂。然后初始化一個指針,使其包含一個RunStruct对雪,其上升高度等于圖像高度河狐,寬度屬性等于圖像寬度。
- 創(chuàng)建一個CTRunDelegateCallbacks,返回屬于RunStruct類型指針的上升馋艺、下降和寬度屬性栅干。
- 使用ctrundelegateccreate創(chuàng)建一個綁定回調(diào)和數(shù)據(jù)參數(shù)的委托實例。
- 創(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 {
}
}
}
}
- 獲取CTFrame的CTLine對象的數(shù)組绝骚。
- 使用CTFrameGetOrigins將ctframe的行原點復(fù)制到origin數(shù)組中毕籽。通過設(shè)置一個長度為0的范圍娃惯,CTFrameGetOrigins將知道要遍歷整個CTFrame儒将。
- 設(shè)置nextImage以包含當(dāng)前圖像的屬性數(shù)據(jù)。如果nextImage包含圖像的位置碗暗,展開它并繼續(xù);否則,提前返回萍摊。
- 循環(huán)文本行倡勇。
- 如果該行的字形運行衫贬,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
}
- 如果當(dāng)前運行的范圍不包含下一個圖像,則跳過循環(huán)的其余部分缴守。否則葬毫,在這里渲染圖像。
- 使用CTRunGetTypographicBounds計算圖像寬度屡穗,并將高度設(shè)置為找到的上升高度贴捡。
- 使用CTLineGetOffsetForStringIndex獲取直線的x偏移量,然后將其添加到imgBounds的原點村砂。
- 將圖像及其幀添加到當(dāng)前的CTColumnView中烂斋。
- 增加圖像索引。如果有一個圖像在images[imageIndex],更新nextImage和imgLocation汛骂,以便它們引用下一個圖像罕模。
好的!太棒了!差不多了,最后一步帘瞭。
添加以下右邊的pageView.addSubview(列)內(nèi)的buildFrames(withAttrString:and imagages:)附加圖像淑掌,如果他們存在:
if images.count > imageIndex {
attachImagesWithFrame(images, ctframe: ctframe, margin: settings.margin, columnView: column)
}
看看iphone 和ipad的效果:
恭喜!為了感謝你們的辛勤工作,僵尸們放過了你們的大腦!:]
哪里可以看到效果?
在這里查看完成的項目蝶念。
正如在介紹中提到的抛腕,Text Kit通常可以替代Core Text;所以試著用Text Kit編寫這個教程媒殉,看看它是如何比較的担敌。也就是說,這堂 Core Text課不會是徒勞的!TextKit提供了免費的橋接到CoreText廷蓉,所以你有需要的話全封,可以很容易地進行框架之間的轉(zhuǎn)換。