core text tutorial for ios : making a magazine app 翻译。core text 是一个底层文本引擎,当与 core graphics/ quartz 框架一起使用时,它可以对布局和格式进行细粒度的控制。
在 ios 7 时候,apple 发布了 textkit 类库,它可以存储、列出和显示带有各种排版特征的文本。虽然 textkit 功能强大,在布局文本时已经足够强大,但相对而言, core text 可以提供更多的控制。例如,如果你需要直接使用 quartz,那么 core text 就可以。如果你需要构建自己的布局引擎,core text 将帮助你生成 “字形”,并将它们相对地放置在精细排版中。
本教程通过使用 core text 创建一个非常简的单杂志应用… zombies !
打开 xcode ,基于 single view application template 创建一个新的 swift universal project ,并命名为 coretextmagazine。
然后,添加 core text framework 到我们的工程中:
1. 单击项目导航器中的项目文件。
2. 在 “general” 下,滚动到下面的 “linked frameworks and libraries”。
3. 单击 “+”, 并搜索 “coretext”。
4. 选择 “coretext.framework” ,然后单击 “add” 按钮。
现在工程已经建好了,是时候开始编码了。
作为开始,我们将创建一个 uiview,在它的 draw(_:) 方法内将使用 core text。
创建一个 cocoa touch class 文件,它继承自 uiview 。将其命名为 ctview 。
打开 ctview.swift,添加如下代码:
import coretext
而后,设置这个自定义的 view 为应用的主视图。打开 main.storyboard,打开右边的 utilities 菜单,选中它上边 toolbar 的 identity inspector 按钮,interface builder 左边菜单,选中 view。utilities 菜单的 class 文本框现在应该为 uiview。键入文本框 ctview ,将其设置为主视图控制器的 view。
然后,打开 ctview.swift ,替换方法如下:
//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. 一旦 view 创建,draw(_:) 将自动执行来渲染 view 下的 layer。
2. 打开我们将用于绘图的当前图形上下文。
3. 创建一个路径,该路径限制绘图区域,整个视图的边界在该控制下。
4. 在 core text 中,使用 nsattributedstring 来保存文本及其属性,而不是 string 或 nsstring。初始 “hello world” 作为一个带属性的字符串。
5. ctframesettercreatewithattributedstring 创建一个使用提供的属性字符串的 ctframesetter,ctframesetter 将管理我们的字体引用和绘图框架。
6. 创建 ctframe,通过拥有 ctframesettercreateframe 使整个字符串在路径中呈现。
7. ctframedraw 在给定的上下文中绘制 ctframe 。
这就是我们要绘制一些简单的文字所需要的所有步骤。 build and run,我们可以看到运行结果。
uh-oh… 这似乎哪里不对,不是吗?和许多低级 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)
此代码通过向视图的上下文中应用转换来翻转内容。
build and run 应用。先忽略状态栏的重叠,以后我们将会学习如何解决这个问题。
恭喜,我们已经创建了属于自己的第一个 core text 应用。
如果你对 ctframesetter 和 ctframe 有点困惑,没关系,我们现在讲解一下它。
以下为 core text 对象模型的样子:
当我们创建一个 ctframesetter 引用并为它提供一个 nsattributedstring 时,将自动创建一个 cttypesetter 实例来管理字体。接下来,使用 ctframesetter 创建一个或多个将呈现文本的框架。
当我们创建一个 frame 时,我们提供文本的 subrange,并在它的矩形内渲染文本。core text 自动为每一行文本创建一个 ctline,每段文本 的 ctline 有相同的格式。例如,如果在一行中有多个红色单词,core text 将为这几个词创建一个 ctrun,然后为剩下的纯文本创建另一个 ctrun,再为另一些粗体的词句创建一个 ctrun,等等。此外,core text 根据提供的 nsattributedstring 的属性为我们创建 ctruns。每一个 ctruns 控件都可以采用不同的属性,因此我们可以对 kerning、ligunk、width、height 等属性进行很好的控制。
下载资源压缩包the zombie magazine materials.
拖拽文件夹到我们的 xcode 工程中,当弹框提醒的时候,确定 copy items if needed 和 create groups 是选中的。
创建 app ,我们需要将各种属性应用于文本。我们将创建一个简单的文本标记解析器,它将使用标记来设置杂志的格式。
创建一个新的 cocoa touch 类,让它继承自 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 red and blue 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开始是空的,但最终会包含解析后的标记。 这个正则表达式,它意思是说,“通过字符串查找,直到找到一个开头的括号,然后查看字符串,直到找到一个结束括号(或者文档的末尾)。” 搜索 regex 匹配的整个标记范围,然后生成一个 nstextcheckingresults 结果数组。
注:想更多了解正则表达式,看这里 nsregularexpression tutorial.
现在我们已经将所有文本和格式化标记解析成块,接下来我们将遍历块来构建属性字符串。
但在此之前,我们是否注意到如何匹配 (in:options:range:) 接受一个 nsrange 作为参数呢?当你将 ns 正则表达式函数应用于你的标记字符串时,将会有很多 nsrange 到 range 转换。swift 一直是我们所有人的好朋友,关键时刻,它给予我们帮助。
在 markupparser.swift 中,将下面的扩展添加到文件的末尾:
// mark: - string extension string { func range(from range: nsrange) -> range? { 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 } }
该函数将字符串的开始和结束索引转换为由 nsrange表示的字符串,string.utf16view.index 格式,即字符串中 utf - 16 代码单元集合中的位置; 然后转换每个 string.utf16view.index 到 string.index 格式。只要索引是有效的,该方法将返回代表原 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) }循环遍历 chunks. 获取当前的 nstextcheckingresult 的范围,打开range
// 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如果 parts.count 小于2,跳过循环体的其余部分。否则,将第二部分存储为 tag。
如果 tag 以 “font” 开始,创建一个 regex 来查找字体的 “color” 值,然后使用该 regex 通过标记的匹配 “color” 值来枚举。在本例中,应该只有一个匹配的颜色值。
如果 enumeratematches(in:options:range:using:) 返回一个有效的 match , match 中含有一个有效的 tag,查找指定的值(ex . returns ” red “),并追加” color “以形成uicolor选择器。
同样,创建一个 regex 来处理字体的 “face” 值。如果找到匹配,则将 fontname 设置为该字符串。great job! 现在,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(_:) 中删除 attrstring = nsattributedstring(string: “hello world”)。
在这里,我们创建了一个实例变量来保存带属性的字符串,以及将其从 app 的其他地方设置的方法。
接下来, 打开 viewcontroller.wift 并将以下内容添加到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 中的文本到一个 string。 创建一个新的解析器,在文本中输入,然后将返回的属性字符串传递给 viewcontroller 的 ctview。build and run the app!
太棒了!得助于 50 行解析代码,我们可以简单地使用一个文本文件来保存杂志应用程序的内容了。
如果你认为一个僵尸新闻的月杂志可以放到一个微不足道的页面上,那你就大错特错了! 幸运的是 core text 布局列时尤为有用, ctframegetvisiblestringrange 可以告诉我们多少文本将适合一个给定的框架。也就是说,你可以创建一个列,然后当它的全部被填充后,你可以再创建另一个列,等等。
对于这个 app,你必须打印列,然后是页面,然后是一本完整的杂志,以免冒犯不死族,所以……是时候将 ctview 子类转换为 uiscrollview 了。
打开 ctview.swift 和更改类 ctview 一行:
class ctview: uiscrollview {
看到僵尸了吧? 这个应用现在可以支持不死的冒险了! 是的——仅仅一行代码,就可以滚动和分页了。
到目前为止,我们已经创建了框架和框架内 draw(_:),但是由于我们有许多不同格式的列,所以最好创建单独的列实例。
创建一个新的 cocoa touch 类文件,命名为 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(框架:ctframe:) 集合:
the view’s frame. 在上下文渲染的 ctframe。 设置 view 的背景颜色为白色。接下来,创建一个名为 ctsettings.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),每页的列数,每一页的 frame,每一页 frame 大小。 由于该杂志同时提供 iphone 和 ipad 上的僵尸,在 ipad 上显示两列,在 iphone 上显示一列,因此每一个屏幕的大小都是合适的。 设置 pagerect 为 uiscreen.main.bounds.insetby(dx: margin, dy: margin)。 设置 columnrect 为将 pagerect 的宽度除以每一页的列数,并除去边距。
打开 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 允许你访问应用程序的 margin 大小,每一页的列,page frame 和 column frame 设置。
5. 我们将遍历 attrstring 并按列列出文本列,直到当前文本位置到达结束为止。
是时候添加 looping attrstring 了,在 while textpos < attrstring.length {.: 方法内添加如下代码:
//1 if columnindex.truncatingremainder(pidingby: 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)如果 column index 被每页的列数相除等于 0 ,则表示该列是其页面上的第一个列,创建一个新的 page视图来保存列。使用边缘 settings.pagerect 设置它的帧。x offset 为当前页 index 乘以屏幕宽度。当页面滚动时,每个杂志页面将位于前一个页面的右侧。 自增 pageindex。 pageview 的宽度除以 settings.columnsperpage 获得第一列的 x 坐标; x 坐标乘以 column index 获得列偏移。然后用标准列向量来创建当前列的 frame,并通过 columnoffset 来抵消它的 x 原点。
接下来,在 columnframe initialization 下面添加如下代码。
//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,包含尽可能多的文本。 使用 cgrect columnframe 和 ctframe ctframe 创建一个 ctcolumnview ,添加列到 pageview。 使用 ctframegetvisiblestringrange(_:) 来计算文本列中包含的范围,然后 textpos +framerange.length 来反映当前文本的位置。 在循环到下一列之前,将列索引增加1。
最后,在循环之后设置滚动视图的内容大小:
contentsize = cgsize(width: cgfloat(pageindex) * bounds.size.width, height: bounds.size.height)
通过将内容大小设置为屏幕宽度乘以页面数,zombies 现在可以滚动到最后了。
打开 viewcontroller.swift ,替换
(view as? ctview)?.importattrstring(parser.attrstring)
为
(view as? ctview)?.buildframes(withattrstring: parser.attrstring, andimages: parser.images)
在 ipad 上运行 app,左右拖动到页面之间,检查双列布局。看起来不错.:]
我们有了列和格式化的文本,但还缺少图像。使用 core text 绘制图像并不是那么简单——毕竟它是一个文本框架——但是在我们已经创建的标记解析器的帮助下,添加图像不应该太糟糕。
虽然 core text 不能绘制图像,但作为一个布局引擎,它可以留出空白空间来为图像腾出空间。通过设置 ctrun 的 delegate,我们可以确定 ctrun 的 ascent space, descent space and width。像下面这样:
当 core text 获得一个带有 ctrundelegate 的 ctrun 类时,它会询问委托,“我应该留出多少空间来处理这段数据?” 通过在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) } } }如果 tag 以 “img” 开始,使用正则表达式寻找 图像的 “src” ,即 filename。 将图像宽设置为列的宽度,并设置其高度,使图像保持其高宽比。 如果图像的高度太长,则设置高为适合的列,并减小宽度以保持图像的纵横比。由于图像后面的文本将包含空的空间属性,包含空空间信息的文本必须与图像匹配在同一列中。设置图像的高度为 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.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))赋值字典给变量 images,字典包含 image’s size, filename and text location。 定义 runstruct 来保存描述空空间的属性。然后初始化一个指针,以包含一个 ascent 等于图像高度的 runstruct,以及一个与图像宽度相等的 width 属性。 创建一个 ctrundelegatecallbacks ,它返回类型指针 runstruct 的 ascent, descent 以及 width 属性。 使用 ctrundelegatecreate 创建一个 delegate 实例,来绑定 callbacks 和 参数数据(data parameter)。 创建一个包含 delegate 实例的属性字典,然后赋值空字符串给 attrstring, attrstring 包含了文本中空洞的位置和大小信息。
现在,markupparser 正在处理“img”标记,我们需要调整 ctcolumnview 和 ctview 来呈现它们。
打开 ctcolumnview.swift,在 var ctframe:ctframe! 添加如下代码,以此控制列中的图片和frames:
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) } }
这里我们遍历每个 image 并绘制它到合适的 frame。
接下来,打开 ctview.swift 并在 class 的顶部添加如下属性:
// mark: - properties var imageindex: int!
当你绘制 ctcolumnviews 时,imageindex 将追踪当前的图像 index。接下来,在 buildframes(withattrstring:andimages:) 上面添加如下代码:
imageindex = 0
这标记 images 数组的第一个元素。
接着,在 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.. 获取 ctframe’s ctline objects 的数组。 使用 ctframegetorigins 拷贝 ctframe’s line origins 到 origins array。通过设置 range length 为 0,ctframegetorigins 知道穿越整个 ctframe。 设置 nextimage 来包含当前图像的属性数据。如果 nextimage 包含图像的位置,打开它并继续;否则,提前返回。 循环遍历 text’s lines 。 如果 line’s glyph runs, filename 和 filename 的image 都存在,循环 glyph runs 。接下来,添加如下代码到 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 计算 image width, 将 width 赋值给 ascent。 使用 ctlinegetoffsetforstringindex 获取 line 的 x offset,然后添加它到 imgbounds’ origin。 添加 image 和它的 frame到当前 ctcolumnview。 增加 imageindex。如果有图像在 imges 数组中,更新 nextimage 和 imglocation,以便它们指向下一个图像。ok! great! 只剩下最后一步了。
在方法 buildframes(withattrstring:andimages:) 中pageview.addsubview(column) 的上部添加如下代码:
if images.count > imageindex { attachimageswithframe(images, ctframe: ctframe, margin: settings.margin, columnview: column) }如果它们存在,附加图像。
在 iphone 和 ipad 上面运行,效果如下:
恭喜,大功告成。
如对本文有疑问, 点击进行留言回复!!
纵横字谜的答案 Crossword Answers, ACM/ICPC World Finals 1994, UVa232
HDU - 5880 Family View (AC自动机修改母串)
iOS14Beta3续航怎么样 iOS14Beta3续航能力介绍
iOS14Beta3稳定性怎么样 iOS14Beta3升级建议介绍
网友评论