原文:Core Text Tutorial for iOS: Making a Magazine App
更新说明:本教程已由Lyndsey Scott更新到Swift 4和Xcode 9。最初的教程是由Marin Todorov编写的。
Core Text是一个low-level 文本引擎,当与Core Graphics/Quartz框架一起使用时,它可以让你对布局和格式进行粒度更加精细的控制。
在iOS 7中,苹果发布了一个名为Text Kit的高级库,用于存储、布局和显示具有各种排版特征的文本。虽然Text Kit是强大的,通常在布局文本时是足够的,但Core Text可以提供更多的控制。例如,如果你需要直接使用Quartz,使用Core Text。如果你需要创建自己的布局引擎,Core Text将帮助你生成“ glyphs,以及它们彼此精细排版后相对定位。”
本教程将带你通过使用Core Text创建一个非常简单的杂志应用程序的过程……
哦,《僵尸月刊》的读者们已经同意,只要你在本教程中还在使用它们,他们就不会吃你的大脑……所以你可能想要尽快开始!
注意:要充分利用本教程,你需要首先了解iOS开发的基础知识。如果你是iOS开发新手,你应该先看看这个网站上的其他教程。
开始
打开Xcode,用Single View Application Template创建一个新的Swift通用项目,命名为CoreTextMagazine。
接下来,将Core Text框架添加到项目中:
在项目导航器中单击项目文件(左侧的条带)
在“常规”下,向下滚动到底部的“链接框架和库”
点击“+”搜索“CoreText”
选择“CoreText.framework”,点击“Add”按钮。就是这样!
项目设置已经完成,该开始敲代码了。
添加一个Core Text View
对于初学者来说,你将创建一个自定义的UIView,它将在draw(_:)
方法中使用Core Text。
创建一个新的Cocoa Touch 类文件,继承与UIView,类名CTView。
打开CTView.swift,并在import UIKit下添加以下内容:
import CoreText
接下来,将这个新的自定义视图设置为应用程序中的主视图。打开Main.storyboard,打开右手边的Utilities菜单,然后在它的顶部工具栏中选择Identity Inspector图标。在Interface Builder的左侧菜单中,选择View。工具菜单的Class字段现在应该显示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)
}
让我们一步一步来。
在视图创建时,draw(_:)将自动运行以渲染视图的backing layer。
展开将用于绘图的当前图形上下文。
创建一个路径来限制绘制区域,在这个例子中是整个视图的边界
在Core Text中,你使用NSAttributedString,而不是String或NSString,来保存文本和它的属性。初始化“Hello World”为带属性字符串。
CTFramesetterCreateWithAttributedString使用提供的带属性字符串创建一个CTFramesetter。CTFramesetter将管理你的字体参考和你的绘图框架。
通过使用CTFramesetterCreateFrame渲染路径内的整个字符串,创建一个CTFrame。
CTFrameDraw在给定的上下文中绘制CTFrame。
这就是绘制一些简单文本所需要的全部内容!构建、运行并查看结果。
这似乎不对,是吗?像许多低级的api一样,Core Text使用y -翻转的坐标系统。更糟糕的是,内容也是垂直翻转的!
在guard let context语句下面直接添加以下代码,以修复内容方向:
// Flip the coordinate system
context.textMatrix = .identity
context.translateBy(x: 0, y: bounds.size.height)
context.scaleBy(x: 1.0, y: -1.0)
这段代码通过将转换应用到视图的上下文来翻转内容。
构建并运行这个应用程序。不要担心状态栏重叠,稍后你将学习如何用边距修复这个问题。
祝贺你的第一个核心文本应用!僵尸们对你的进展很满意。
Core Text Object Model
如果你对CTFramesetter和CTFrame有点困惑,那没关系,因为是时候澄清一下了。:]
Core Text对象模型是这样的:
当你创建一个CTFramesetter引用并为它提供一个NSAttributedString时,一个CTTypesetter的实例就会被自动创建以供你管理字体。接下来,使用CTFramesetter创建一个或多个用来呈现文本的帧。
当您创建一个框架时,您为它提供要在其矩形内呈现的文本子范围。Core Text自动为每一行文本创建一个CTLine,为每一段具有相同格式的文本创建一个CTRun。例如,Core Text会创建一个CTRun,如果你在一行中有几个红色的单词,然后另一个CTRun用于下面的纯文本,然后另一个CTRun用于一个粗体句子,等等。Core Text为你创建基于提供的NSAttributedString属性的CTRun。此外,每个CTRun对象都可以采用不同的属性,因此您可以很好地控制字距、连接、宽度、高度等。
进入杂志应用程序!
下载并解压缩僵尸杂志材料。
把这个文件夹拖到你的Xcode项目中。当出现提示时,请确保选中了Copy items if needed 和 Create groups。
要创建应用程序,您需要对文本应用各种属性。您将创建一个简单的文本标记解析器,它将使用标记来设置杂志的格式。
创建一个新的CocoaTouch Class文件,继承NSObject,命名为MarkupParser。
首先,快速浏览一下zombies.txt。看到它是如何在整个文本中包含括弧格式标记的了吗?
" img src "标记参考杂志图像
"font color/face "标记决定文本颜色和字体。
打开MarkupParser.swift,并将其内容替换为以下内容:
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) {
}
}
这里你添加了属性来保存字体和文本颜色;设置默认值;创建一个变量来保存由parseMarkup(_:)产生的带属性字符串;并创建了一个数组,该数组最终将保存定义文本中图像的大小、位置和文件名的字典信息。
编写一个解析器通常是很困难的工作,但是本教程的解析器将非常简单,并且只支持开始标记—这意味着一个标记将设置它后面文本的样式,直到找到一个新标记。文本标记看起来像这样:
These are <font color="red">red<font color="black"> and
<font color="blue">blue <font color="black">words.
并产生这样的输出:
这些是红色和蓝色的单词。
我们开始尝试解析!
将以下内容添加到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一开始为空,但最终将包含解析后的标记。
- 这个正则表达式将文本块与紧跟其后的标签进行匹配。它说,“遍历字符串直到你找到一个开始的括号,然后遍历字符串直到你碰到一个结束的括号(或文档的结尾)。”
- 搜索正则表达式匹配的标记的整个范围,然后生成生成的NSTextCheckingResults的数组。
注意:要了解更多关于正则表达式的知识,请查看NSRegularExpression教程。
现在您已经将所有文本和格式化标记解析为块,接下来将遍历块以构建带属性字符串。
但是在那之前,你注意到match(在:options:range:)是如何接受一个NSRange作为参数的吗?当你将nsregulareexpression函数应用到你的标记字符串时,会有很多NSRange到Range的转换。斯威夫特是我们所有人的好朋友,所以它应该得到帮助。
仍然在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
}
}
(不好意思,这块没看懂)
这个函数将字符串的开始和结束索引转换为 utf16view格式编码,即字符串的UTF-16代码单元集合中的位置;然后转换每个String.UTF16View。索引的字符串。指数格式;当合并时,生成Swift的range格式:range。只要索引是有效的,该方法将返回原始NSRange的Range表示。
你的Swift现在很冷静。现在回到处理文本和标记块的问题。
在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)
}
- 循环块。
- 获取当前NSTextCheckingResult的range,打开range <String。索引>,并继续处理块,只要它存在。
- 用“<”分隔块。第一部分包含杂志文本,第二部分包含标签(如果存在的话)。
- 使用fontName创建一个字体,当前默认为“Arial”,以及一个相对于设备屏幕的大小。如果fontName没有产生一个有效的UIFont,设置字体为默认字体。
- 创建一个字体格式的字典,将其应用于部分[0]以创建带属性字符串,然后将该字符串附加到结果字符串。
要处理"font"标签,在attrString.append(text)后面插入以下内容:
// 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
- 如果少于两个部分,则跳过循环体的其余部分。否则,将第二部分存储为标记。
- 如果标签以"font"开头,创建一个正则表达式来查找字体的"color"值,然后使用该正则表达式来枚举标签匹配的"color"值。在这种情况下,应该只有一个匹配的颜色值。
- 如果enumerateMatches(在:options:range:using:)返回一个有效的匹配与标签中的有效范围,找到指定的值(例如<font color="red">返回"red")并添加" color "以形成一个UIColor选择器。执行该选择器,然后将类的颜色设置为返回的颜色(如果存在),如果不存在则设置为黑色。
- 类似地,创建一个正则表达式来处理字体的“face”值。如果找到匹配,设置fontName为该字符串。
伟大的工作!现在parseMarkup(_:)可以获取标记并为Core Text生成一个NSAttributedString。
是时候把你的应用喂给僵尸了!我的意思是,在你的应用中添加一些僵尸……zombies.txt。,)
UIView的工作是显示给它的内容,而不是加载内容。打开CTView.swift,并添加以下绘制draw(_:):
// MARK: - Properties
var attrString: NSAttributedString!
// MARK: - Internal
func importAttrString(_ attrString: NSAttributedString) {
self.attrString = attrString
}
接下来,从draw(_:)中删除let attrString = NSAttributedString(string: "Hello World")。
在这里,您创建了一个实例变量来保存带属性字符串,并创建了一个方法来从应用程序的其他地方设置它。
接下来,打开ViewController.swift,并在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 _ {
}
让我们一步一步来。
- 从zombie.txt文件中加载文本到字符串中。
- 创建一个新的解析器,输入文本,然后将返回的带属性字符串传递给ViewController的CTView。
-
构建并运行应用程序!
太棒了?多亏了大约50行解析,你可以简单地使用一个文本文件来保存杂志应用程序的内容。
杂志的基本布局
如果你认为一本关于僵尸新闻的月刊可以放在一页纸里,那你就大错特错了!幸运的是,Core Text在布局列时变得特别有用,因为CTFrameGetVisibleStringRange可以告诉你有多少文本适合给定的框架。意思是,你可以创建一个列,当它满了,你可以创建另一个列,等等。
在这个应用程序中,你必须打印专栏,然后是页面,然后是整本杂志,以免冒犯不死族,所以……是时候把你的CTView子类变成UIScrollView了。
打开CTView.swift,将CTView类改为:
class CTView: UIScrollView {
看到这一幕,僵尸?该应用程序现在可以支持一个永恒的不死冒险!是的——只用一行,现在就可以滚动和分页了。
到目前为止,您已经在draw(_:)中创建了框架设置器和框架,但由于您将有许多格式不同的列,所以最好创建单独的列实例。
创建一个新的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)
}
}
这段代码呈现一个CTFrame,就像你在CTView中最初做的那样。自定义初始化器init(frame:ctframe:)
设置:
视图的框架。
要绘制到上下文中的CTFrame。
视图的背景色为白色。
接下来,创建一个名为CTSettings.swift的新swift文件,它将保存您的列设置。
将CTSettings.swift中的内容替换为以下内容:
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)
}
}
- 属性将决定页边距(本教程默认为20);每页列数;每页包含列的框架;以及每页每栏的帧大小。
- 由于该杂志同时面向iPhone和iPad,所以在iPad上显示两个栏目,在iPhone上显示一个栏目,所以栏目的数量适合不同的屏幕尺寸。
- 根据页边距的大小插入页面的整个边界,以计算pag直立。
将pag直立的宽度除以每页的列数,并在新框架中插入columnRect的边距。
打开,CTView.swift,替换整个内容如下:
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:)
将创建CTColumnViews,然后将它们添加到滚动视图。 - 启用滚动视图的分页行为;因此,每当用户停止滚动时,滚动视图就会立即进入位置,因此每次只显示一个完整的页面。
- 框架设置器将创建每个列的带属性文本的CTFrame。
- UIView页面视图将作为每个页面的列子视图的容器;textPos将跟踪下一个字符;columnIndex将跟踪当前列;pageIndex将跟踪当前页面;“设置”可以让你访问应用程序的页边距大小、每页的列数、页面框架和列框架设置。
- 你将循环遍历attrString并一列一列地布局文本,直到当前文本位置到达末尾。
开始循环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)
- 如果列索引除以每页的列数等于0,从而表明该列是其页面上的第一个列,则创建一个新的页面视图来保存这些列。要设置它的框架,取边距设置。pag竖立和偏移其x原点的当前页面索引乘以屏幕的宽度;因此,在分页滚动视图中,每个杂志页面都将在前一个页面的右侧。
- 增加pageIndex。
- 通过设置划分页面视图的宽度。columnsPerPage获取第一列的x原点;用这个原点乘以列索引得到列的偏移量;然后通过获取标准的columnRect并通过columnOffset偏移其x原点来创建当前列的框架。
接下来,添加以下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
- 创建一个CGMutablePath列的大小,然后从textPos开始,呈现一个新的CTFrame与尽可能多的文本。
- 创建一个CTColumnView与CGRect columnFrame和CTFrame CTFrame然后添加列pageView。
- 使用ctframegetvisiblestringgrange(_:)计算列中包含的文本范围,然后将textPos递增该范围长度以反映当前文本位置。
- 在循环到下一列之前,将列索引递增1。
最后,在循环之后设置滚动视图的内容大小:
contentSize = CGSize(width: CGFloat(pageIndex) * bounds.size.width,
height: bounds.size.height)
通过设置内容大小为屏幕宽度乘以页面数量,僵尸现在可以滚动到最后。
打开ViewController.swift,并替换
(view as? CTView)?.importAttrString(parser.attrString)
为
(view as? CTView)?.buildFrames(withAttrString: parser.attrString, andImages: parser.images)
在iPad上构建并运行应用程序。检查双列布局!左右拖动可以在页面之间切换。看起来似乎不错 😄
您有列和格式化的文本,但您缺少图像。用Core Text绘制图像并不是那么简单——毕竟它是一个文本框架——但是在你已经创建的标记解析器的帮助下,添加图像应该不会太糟糕。
在核心文本绘制图像
虽然Core Text不能绘制图像,但作为一个布局引擎,它可以为图像留出空间。通过设置CTRun的委托,可以确定CTRun的上升空间、下降空间和宽度。像这样:
当Core Text到达一个带有CTRunDelegate的CTRun时,它会问delegate,“我应该为这个数据块留下多少空间?”通过在CTRunDelegate中设置这些属性,您可以在您的图像的文本中留下空间。
首先添加对“img”标签的支持。打开MarkupParser.swift,找到"} //end of font parsing".。在后面立即添加以下内容:
//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)
}
}
}
- 如果标签以"img"开头,使用regex搜索图像的"src"值,即文件名。
- 将图像宽度设置为列的宽度,并设置其高度,以便图像保持其高度-宽度宽高比。
- 如果图像的高度对于列来说太长,则设置高度以适应列,并减少宽度以保持图像的长宽比。由于图像后面的文本将包含空格属性,所以包含空格信息的文本必须适合于图像的同一列;因此将图像高度设置为settings.columnRect.height - font.lineHeight。
接下来,在if let图像块之后添加如下内容:
//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,其上升高度等于图像高度,宽度属性等于图像宽度。
- 创建一个CTRunDelegateCallbacks,返回属于RunStruct类型指针的上升、下降和宽度属性。
- 使用ctrundelegateccreate创建一个绑定回调和数据参数的委托实例。
- 创建一个包含委托实例的带属性字典,然后向attrString追加一个空格,它保存文本中空洞的位置和大小信息。
现在MarkupParser处理“img”标签,你需要调整CTColumnView和CTView渲染他们。
CTColumnView.swift开放。添加以下var ctFrame: ctFrame !保存柱子的图像和框架:
var images: [(image: UIImage, frame: CGRect)] = []
接下来,将以下内容添加到draw(_:)的底部:
for imageData in images {
if let image = imageData.image.cgImage {
let imgBounds = imageData.frame
context.draw(image, in: imgBounds)
}
}
在这里,您可以遍历每个图像,并将其绘制到适当框架的上下文中。
接下来打开CTView.swift和下面的属性到类的顶部:
// MARK: - Properties
var imageIndex: Int!
imageIndex将在你绘制CTColumnViews时跟踪当前的图像索引。
接下来,添加以下内容到buildFrames(withAttrString:andImages:)的顶部:
imageIndex = 0
这标记了图像数组的第一个元素。
接下来添加下面的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对象的数组。
- 使用CTFrameGetOrigins将ctframe的行原点复制到origin数组中。通过设置一个长度为0的范围,CTFrameGetOrigins将知道要遍历整个CTFrame。
- 设置nextImage以包含当前图像的属性数据。如果nextImage包含图像的位置,展开它并继续;否则,提前返回。
- 循环文本行。
- 如果该行的字形运行,filename和image with filename都存在,则循环执行该行的字形运行。
接下来,在符号run for-loop中添加以下内容:
// 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
}
- 如果当前运行的范围不包含下一个图像,则跳过循环的其余部分。否则,在这里渲染图像。
- 使用CTRunGetTypographicBounds计算图像宽度,并将高度设置为找到的上升高度。
- 使用CTLineGetOffsetForStringIndex获取直线的x偏移量,然后将其添加到imgBounds的原点。
- 将图像及其帧添加到当前的CTColumnView中。
- 增加图像索引。如果有一个图像在images[imageIndex],更新nextImage和imgLocation,以便它们引用下一个图像。
好的!太棒了!差不多了,最后一步。
添加以下右边的pageView.addSubview(列)内的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,所以你有需要的话,可以很容易地进行框架之间的转换。