版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.08.27 |
前言
Core Text
框架主要用来做文字处理,是的iOS3.2+
和OSX10.5+
中的文本引擎,让您精细的控制文本布局和格式。它位于在UIKit
中和CoreGraphics/Quartz
之间的最佳点。接下来这几篇我们就主要解析该框架。感兴趣的可以前面几篇。
1. Core Text框架详细解析(一) —— 基本概览
2. Core Text框架详细解析(二) —— 关于Core Text
3. Core Text框架详细解析(三) —— Core Text总体概览
4. Core Text框架详细解析(四) —— Core Text文本布局操作
5. Core Text框架详细解析(五) —— Core Text字体操作
开始
首先看一下本文的写作环境:
Swift 4, iOS 11, Xcode 9
Core Text是一个低级底层文本引擎,与Core Graphics / Quartz
框架一起使用时,可以对布局和格式进行细粒度控制。
在iOS 7中,Apple发布了一个名为Text Kit
的高级库,它存储,布局和显示具有各种排版特征的文本。尽管Text Kit功能强大且通常在布局文本时足够,但Core Text可以提供更多控制。例如,如果您需要直接使用Quartz,请使用Core Text。如果您需要构建自己的布局引擎,Core Text
将帮助您生成“glyphs and position them relative to each other with all the features of fine typesetting.”。
本教程将指导您使用Core Text
创建一个非常简单的杂志应用程序!
下面就新建立一个工程并开始我们的工作。
打开Xcode,使用Single View Application Template
创建一个新的Swift universal project
,并将其命名为CoreTextMagazine
。
接下来,将Core Text
框架添加到项目中:
- 1)单击项目导航器中的项目文件(左侧的条带)
- 2)在
“General”
下,向下滚动到底部的“Linked Frameworks and Libraries”
- 3)点击
“+”
并搜索“CoreText”
- 4)选择
“CoreText.framework”
并单击“Add”
按钮。 而已!
现在项目已经设置好,是时候开始编码了。
Adding a Core Text View - 添加一个Core Text视图
对于初学者,您将创建一个自定义UIView,它将在其draw(_ :)
方法中使用Core Text
。
创建一个名为CTView
继承自UIView的新Cocoa Touch
类文件。
打开CTView.swift
,并在导入UIKit下添加以下内容:
import CoreText
接下来,将此新自定义视图设置为应用程序中的主视图。 打开Main.storyboard
,打开右侧的Utilities
菜单,然后在其顶部工具栏中选择Identity Inspector
图标。 在Interface Builder
的左侧菜单中,选择View。 Utilities菜单的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)
}
下面,让我们一步一步地回顾一下:
- 1)创建视图后,
draw(_ :)
将自动运行以渲染视图的背景层。 - 2)展开您将用于绘图的当前图形上下文。
- 3)创建一个限制绘图区域的路径,在这种情况下是整个视图的边界
- 4)在Core Text中,使用
NSAttributedString
(而不是String或NSString)来保存文本及其属性。 将“Hello World”
初始化为属性字符串。 - 5)
CTFramesetterCreateWithAttributedString
使用提供的属性字符串创建CTFramesetter
。CTFramesetter
将管理您的字体引用和绘图框架。 - 6)通过让
CTFramesetterCreateFrame
呈现path
中的整个字符串来创建CTFrame
。 - 7)
CTFrameDraw
在给定的上下文中绘制CTFrame
。
这就是你需要绘制一些简单的文字! Build,运行并查看结果。
呃哦......那似乎不对,是吗? 与许多低级API一样,Core Text
使用Y-flipped坐标系。 更糟糕的是,内容也垂直翻转!
在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)
此代码通过对视图的上下文应用转换来翻转内容。
Build并运行应用程序。 不要担心状态栏重叠,您将学习如何使用边距修复此问题。
祝贺您的第一个Core Text应用程序!
The Core Text Object Model - Core Text 对象模型
如果你对CTFramesetter
和CTFrame
感到有点困惑 - 这没关系,因为是时候说明一下了。
以下是Core Text
对象模型的样子:
创建CTFramesetter
引用并为其提供NSAttributedString
时,会自动为您创建CTTypesetter
实例以管理您的字体。 接下来,使用CTFramesetter
创建一个或多个frames
,您将在其中呈现文本。
创建frame
时,为其提供要在其矩形内渲染的文本子范围。 Core Text会自动为每行文本创建一个CTLine
,并为具有相同格式的每段文本创建一个CTRun
。 例如,如果您将多个单词连续显示为红色,则会创建一个CTRun,然后是另一个CTRun用于以下纯文本,然后是另一个用于粗体句子的CTRun等。Core Text根据提供的NSAttributedString
属性为您创建CTRuns。 此外,这些CTRun对象中的每一个都可以采用不同的属性,因此您可以很好地控制字距,连字,宽度,高度等。
Onto the Magazine App!
下载并取消归档 the zombie magazine materials。
将文件夹拖到Xcode项目中。 出现提示时,确保选中Copy items if needed
和Create groups
。
要创建应用程序,您需要将各种属性应用于文本。 您将创建一个简单的文本标记解析器,它将使用标签来设置杂志的格式。
创建一个名为MarkupParser
的新Cocoa Touch
类文件,继承自NSObject。
首先,快速浏览一下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 _ {
}
- 1)
attrString
开始为空,但最终将包含已解析的标记。 - 2)这个正则表达式匹配文本块与标签紧随其后。 它表示,“通过字符串查找,直到找到一个开口括号,然后查看字符串,直到你敲到一个右括号(或文档的末尾)。”
- 3)搜索标记的整个范围以进行
regex
匹配,然后生成结果NSTextCheckingResult
的数组。
现在,您已将所有文本和格式标记解析为chunks
,您将循环遍历chunks
以构建属性字符串。
但在此之前,您是否注意到matches(in:options:range:)
如何接受NSRange
作为参数? 将NSRegularExpression
函数应用于标记String
时,会有很多NSRange
到Range
转换。
仍然在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
}
}
此函数将String
的起始和结束索引(由NSRange表示)转换为String.UTF16View.Index
格式,即字符串的UTF-16
代码单元集合中的位置;然后将每个String.UTF16View.Index
转换为String.Index
格式;组合时,产生Swift的范围格式:Range
。 只要索引有效,该方法将返回原始NSRange的Range表示。
下面是时候回到处理文本和标记块了。
在parseMarkup(_ :)
里面添加以下内容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)获取当前
NSTextCheckingResult
的范围,打开Range <String.Index>
并继续块,只要它存在。 - 3)将
chunk
拆分为由“<”
分隔的部分。 第一部分包含杂志文本,第二部分包含标签(如果存在)。 - 4)使用
fontName
创建字体,默认情况下为“Arial”
,相对于设备屏幕的大小。 如果fontName
未生成有效的UIFont
,请将font
设置为默认字体。 - 5)创建字体格式的字典,将其应用于parts [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
- 1)如果少于两个部分,则跳过循环体的其余部分。否则,将第二部分存储为
tag
。 - 2)如果
tag
以“font”
开头,则创建一个正则表达式以查找字体的“ color”
值,然后使用该正则表达式来枚举tag
的匹配“ color”
值。在这种情况下,应该只有一个匹配的颜色值。 - 3)如果
enumerateMatches(in:options:range:using :)
返回tag
中有效范围的有效match
,请找到指示的值(例如<font color =“red”>
返回“red”
)并将“Color”
附加到表单中一个UIColor选择器。执行该选择器,然后将类的颜色设置为返回的颜色(如果存在),否则设置为黑色。 - 4)同样,创建一个正则表达式来处理字体的
“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”)
。
在这里,您创建了一个实例变量来保存attributed string
,以及一种从应用程序的其他位置设置它的方法。
接下来,打开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 _ {
}
让我们一步一步的细分下。
- 1)将
zombie.txt
文件中的文本加载到String
中。 - 2)创建一个新的解析器,输入文本,然后将返回的属性字符串传递给
ViewController
的CTView
。
Build并运行应用程序!
棒极了? 感谢大约50行解析,您只需使用文本文件来保存杂志应用程序的内容。
后记
本篇主要讲述了基于Core Text的Magazine App的制作,感兴趣的给个赞或者关注~~~