Core Text框架详细解析(七) —— 基于Core Tex
版本记录
版本号 | 时间 |
---|---|
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字体操作
6. Core Text框架详细解析(六) —— 基于Core Text的Magazine App的制作(一)
A Basic Magazine Layout - 基本Magazine布局
如果你认为Zombie
新闻的月刊很可能适合一个可怜的页面,那你就错了! 幸运的是,核心文本在布局列时变得特别有用,因为CTFrameGetVisibleStringRange
可以告诉您文本将适合给定的frame
。 意思是,您可以创建一个列,然后一旦填满了,您就可以创建另一个列,等等。
对于这个应用程序,你将不得不打印列,然后打印页面,然后是整个杂志,所以...是时间将你的CTView
子类变成UIScrollView
。
打开CTView.swift
并将class CTView
行更改为:
class CTView: UIScrollView {
到目前为止,您已经在draw(_ :)
中创建了framesetter
和frame
,但由于您将拥有许多具有不同格式的列,因此最好创建单独的列实例。
创建一个名为CTColumnView
继承自UIView
的新Cocoa Touch
类文件。
打开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 :)
,设置:
- 1)视图的
frame
。 - 2)将
CTFrame
绘制到上下文中。 - 3)并且视图的背景颜色为白色。
接下来,创建一个名为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)
}
}
- 1)属性将确定页边距(本教程的默认值为20)、每页的列数、包含列的每个页面的frame、和每页每列的frame大小。
- 2)由于这个杂志同时为iPhone和iPad提供僵尸,在iPad上显示两列,在iPhone上显示一列,因此列数适合每种屏幕尺寸。
- 3)通过边距的大小插入页面的整个边界以计算
pageRect
。 - 4)将pageRect的宽度除以每页的列数,并插入具有
columnRect
边距的新frame。
打开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 {
}
}
}
- 1)
buildFrames(withAttrString:andImages :)
将创建CTColumnViews
然后将它们添加到scrollview
。 - 2)启用
scrollview
的分页行为; 因此,每当用户停止滚动时,滚动视图就会卡入到位,因此一次只显示整个页面。 - 3)
CTFramesetter
,framesetter
将创建属性文本的每个列的CTFrame
。 - 4)
UIView
,pageViews
将作为每个页面的列子视图的容器,textPos
将跟踪下一个字符,columnIndex
将跟踪当前列,pageIndex
将跟踪当前页面, 和settings
使您可以访问应用程序的边距大小,每页的列数,页面frame和列frame设置。 - 5)您将循环遍历
attrString
并逐列布置文本,直到当前文本位置到达结尾。
是时候开始循环attrString
了。 在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)如果列索引除以每页的列数等于0,从而指示该列是其页面上的第一列,则创建一个新的页面视图来保存列。 要设置其frame,请使用边距
settings.pageRect
并将其x原点偏移当前页面索引乘以屏幕宽度;因此,在分页滚动视图中,每个杂志页面将位于前一个杂志页面的右侧。 - 2)增加
pageIndex
。 - 3)用
settings.columnsPerPage
除以pageView
的宽度,得到第一列的x原点;将该原点乘以列索引以获得列偏移量;然后通过获取标准columnRect
并使用columnOffset
偏移其x原点来创建当前列的frame
。
接下来,在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)创建一个列大小的
CGMutablePath
,然后从textPos
开始,使用尽可能多的文本渲染一个新的CTFrame
。 - 2)使用
CGRect columnFrame
和CTFrame ctframe
创建CTColumnView
,然后将该列添加到pageView
。 - 3)使用
CTFrameGetVisibleStringRange(_ :)
计算列中包含的文本范围,然后按该范围长度增加textPos
以反映当前文本位置。 - 4)在循环到下一列之前,将列索引递增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
绘制图像并不是那么简单 - 毕竟它是一个文本框架 - 但是在你已经创建的标记解析器的帮助下,添加图像应该不会太糟糕。
Drawing Images in Core Text - 使用Core Text绘制图像
尽管Core Text
无法绘制图像,但作为布局引擎,它可以留下空白空间以为图像腾出空间。 通过设置CTRun
的代理,您可以确定CTRun的上升空间,下降空间和宽度。 像这样:
当Core Text
通过CTRunDelegate
到达CTRun
时,它会询问代理,“我应该为这块数据留出多少空间?” 通过在CTRunDelegate
中设置这些属性,您可以在图像的文本中留下空洞。
首先添加对“img”
标记(tag)
的支持。 打开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)
}
}
}
- 1)如果标签
tag
以“img”
开头,请使用正则表达式搜索图像的“src”
值,即文件名。 - 2)将图像宽度设置为列的宽度并设置其高度,以使图像保持其高宽宽高比。
- 3)如果图像的高度对于列太长,请将高度设置为适合列,并减小宽度以保持图像的纵横比。 由于图像后面的文本将包含空的空间属性,因此包含空白空间信息的文本必须与图像位于同一列中;所以将图像高度设置为
settings.columnRect.height - font.lineHeight
。
接下来,在if let image
块之后立即添加以下内容:
//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)将包含图像大小,文件名和文本位置的词典附加到
images
。 - 2)定义
RunStruct
以保存将描述空白空间的属性。 然后初始化一个指针,使其包含一个RunStruct
,其ascent
等于图像高度,width
属性等于图像宽度。 - 3)创建一个
CTRunDelegateCallbacks
,它返回属于RunStruct
类型指针的ascent
,descent
和width
属性。 - 4)使用
CTRunDelegateCreate
创建一个代理实例,将回调和数据参数绑定在一起。 - 5)创建一个包含代理实例的属性字典,然后在
attrString
中附加一个空格,该空格保存文本中孔的位置和大小调整信息。
现在MarkupParser
正在处理“img”
标签,你需要调整CTColumnView
和CTView
来渲染它们。
打开CTColumnView.swift
。 在var ctFrame:CTFrame!
下面添加以下内容保持列的图像和frame:
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)
}
}
在这里,您遍历每个图像并将其绘制到其合适的frame内的上下文中。
接下来打开CTView.swift
并将以下属性打开到类的顶部:
// MARK: - Properties
var imageIndex: Int!
在绘制CTColumnViews
时,imageIndex
将跟踪当前图像索引。
接下来,将以下内容添加到buildFrames(withAttrString:andImages:)
的顶部:
imageIndex = 0
这标志着images
数组的第一个元素。
接下来添加以下内容: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
对象。 - 2)使用
CTFrameGetOrigins
将ctframe
的行原点复制到origins
数组中。 通过设置长度为0的范围,CTFrameGetOrigins
将知道遍历整个CTFrame
。 - 3)设置
nextImage
以包含当前图像的属性数据。 如果nextImage包含图像的位置,则打开它并继续;否则,return。 - 4)循环文本的行。
- 5)如果行的字形运行,文件名和图像都存在,则循环遍历该行的字形运行。
接下来,在字形运行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
}
- 1)如果当前运行的范围不包含下一个图像,则跳过循环的其余部分。 否则,在此处渲染图像。
- 2)使用
CTRunGetTypographicBounds
计算图像宽度,并将高度设置为找到的ascent
。 - 3)使用
CTLineGetOffsetForStringIndex
获取行的x偏移,然后将其添加到imgBounds
的原点。 - 4)将图像及其frame添加到当前
CTColumnView
。 - 5)增加图像索引。 如果
images[imageIndex]
上有图像,请更新nextImage
和imgLocation
,以便它们引用下一个图像。
到最后一步了。
在buildFrames(withAttrString:andImages :)
中的pageView.addSubview(column)
上方添加以下内容,以附加图像(如果存在):
if images.count > imageIndex {
attachImagesWithFrame(images, ctframe: ctframe, margin: settings.margin, columnView: column)
}
在iPhone和iPad上构建和运行!
恭喜!已经成功显示了!
如介绍中所述,Text Kit
通常可以替换Core Text
;所以尝试使用Text Kit编写相同的教程,看看它是如何比较的。 也就是说,这篇Core Text
不会徒劳无功! Text Kit为Core Text提供免费桥接,因此您可以根据需要轻松地在框架之间进行转换。
源码
1. ViewController.swift
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.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)?.buildFrames(withAttrString: parser.attrString, andImages: parser.images)
} catch _ {
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
2. CTView.swift
import UIKit
import CoreText
class CTView: UIScrollView {
// MARK: - Properties
var imageIndex: Int!
//1
func buildFrames(withAttrString attrString: NSAttributedString,
andImages images: [[String: Any]]) {
imageIndex = 0
//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
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
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)
if images.count > imageIndex {
attachImagesWithFrame(images, ctframe: ctframe, margin: settings.margin, columnView: column)
}
pageView.addSubview(column)
//3
let frameRange = CTFrameGetVisibleStringRange(ctframe)
textPos += frameRange.length
//4
columnIndex += 1
}
contentSize = CGSize(width: CGFloat(pageIndex) * bounds.size.width,
height: bounds.size.height)
}
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
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
}
}
}
}
}
}
3. 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) {
//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))
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
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
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
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))
}
}
} catch _ {
}
}
}
// 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
}
}
4. CTColumnView.swift
import UIKit
import CoreText
class CTColumnView: UIView {
// MARK: - Properties
var ctFrame: CTFrame!
var images: [(image: UIImage, frame: CGRect)] = []
// 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)
for imageData in images {
if let image = imageData.image.cgImage {
let imgBounds = imageData.frame
context.draw(image, in: imgBounds)
}
}
}
}
5. 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)
}
}
下面看一下运行效果:
后记
本篇主要讲述了基于Core Text的Magazine App的制作,感兴趣的给个赞或者关注~~~