美文网首页
翻译 - Core Text Tutorial for iOS:

翻译 - Core Text Tutorial for iOS:

作者: 携一两本单色书来 | 来源:发表于2022-01-29 14:23 被阅读0次

    原文: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键。


    image

    接下来,打开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。
    这就是绘制一些简单文本所需要的全部内容!构建、运行并查看结果。


    image

    这似乎不对,是吗?像许多低级的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)
    

    这段代码通过将转换应用到视图的上下文来翻转内容。

    构建并运行这个应用程序。不要担心状态栏重叠,稍后你将学习如何用边距修复这个问题。


    image

    祝贺你的第一个核心文本应用!僵尸们对你的进展很满意。

    Core Text Object Model

    如果你对CTFramesetter和CTFrame有点困惑,那没关系,因为是时候澄清一下了。:]

    Core Text对象模型是这样的:


    image

    当你创建一个CTFramesetter引用并为它提供一个NSAttributedString时,一个CTTypesetter的实例就会被自动创建以供你管理字体。接下来,使用CTFramesetter创建一个或多个用来呈现文本的帧。

    当您创建一个框架时,您为它提供要在其矩形内呈现的文本子范围。Core Text自动为每一行文本创建一个CTLine,为每一段具有相同格式的文本创建一个CTRun。例如,Core Text会创建一个CTRun,如果你在一行中有几个红色的单词,然后另一个CTRun用于下面的纯文本,然后另一个CTRun用于一个粗体句子,等等。Core Text为你创建基于提供的NSAttributedString属性的CTRun。此外,每个CTRun对象都可以采用不同的属性,因此您可以很好地控制字距、连接、宽度、高度等。

    进入杂志应用程序!

    下载并解压缩僵尸杂志材料
    把这个文件夹拖到你的Xcode项目中。当出现提示时,请确保选中了Copy items if neededCreate 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 _ {
    }
    
    1. attrString一开始为空,但最终将包含解析后的标记。
    2. 这个正则表达式将文本块与紧跟其后的标签进行匹配。它说,“遍历字符串直到你找到一个开始的括号,然后遍历字符串直到你碰到一个结束的括号(或文档的结尾)。”
    3. 搜索正则表达式匹配的标记的整个范围,然后生成生成的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现在很冷静。现在回到处理文本和标记块的问题。


    image

    在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)
    }
    
    1. 循环块。
    2. 获取当前NSTextCheckingResult的range,打开range <String。索引>,并继续处理块,只要它存在。
    3. 用“<”分隔块。第一部分包含杂志文本,第二部分包含标签(如果存在的话)。
    4. 使用fontName创建一个字体,当前默认为“Arial”,以及一个相对于设备屏幕的大小。如果fontName没有产生一个有效的UIFont,设置字体为默认字体。
    5. 创建一个字体格式的字典,将其应用于部分[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. 如果少于两个部分,则跳过循环体的其余部分。否则,将第二部分存储为标记。
    2. 如果标签以"font"开头,创建一个正则表达式来查找字体的"color"值,然后使用该正则表达式来枚举标签匹配的"color"值。在这种情况下,应该只有一个匹配的颜色值。
    3. 如果enumerateMatches(在:options:range:using:)返回一个有效的匹配与标签中的有效范围,找到指定的值(例如<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")。

    在这里,您创建了一个实例变量来保存带属性字符串,并创建了一个方法来从应用程序的其他地方设置它。

    接下来,打开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文件中加载文本到字符串中。
    2. 创建一个新的解析器,输入文本,然后将返回的带属性字符串传递给ViewController的CTView。
    3. 构建并运行应用程序!


      image

    太棒了?多亏了大约50行解析,你可以简单地使用一个文本文件来保存杂志应用程序的内容。

    杂志的基本布局

    如果你认为一本关于僵尸新闻的月刊可以放在一页纸里,那你就大错特错了!幸运的是,Core Text在布局列时变得特别有用,因为CTFrameGetVisibleStringRange可以告诉你有多少文本适合给定的框架。意思是,你可以创建一个列,当它满了,你可以创建另一个列,等等。

    在这个应用程序中,你必须打印专栏,然后是页面,然后是整本杂志,以免冒犯不死族,所以……是时候把你的CTView子类变成UIScrollView了。

    打开CTView.swift,将CTView类改为:

    class CTView: UIScrollView {

    看到这一幕,僵尸?该应用程序现在可以支持一个永恒的不死冒险!是的——只用一行,现在就可以滚动和分页了。

    image

    到目前为止,您已经在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)
      }
    }
    
    1. 属性将决定页边距(本教程默认为20);每页列数;每页包含列的框架;以及每页每栏的帧大小。
    2. 由于该杂志同时面向iPhone和iPad,所以在iPad上显示两个栏目,在iPhone上显示一个栏目,所以栏目的数量适合不同的屏幕尺寸。
    3. 根据页边距的大小插入页面的整个边界,以计算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 {
        }
      }
    }
    
    1. buildFrames(withAttrString:andImages:)将创建CTColumnViews,然后将它们添加到滚动视图。
    2. 启用滚动视图的分页行为;因此,每当用户停止滚动时,滚动视图就会立即进入位置,因此每次只显示一个完整的页面。
    3. 框架设置器将创建每个列的带属性文本的CTFrame。
    4. UIView页面视图将作为每个页面的列子视图的容器;textPos将跟踪下一个字符;columnIndex将跟踪当前列;pageIndex将跟踪当前页面;“设置”可以让你访问应用程序的页边距大小、每页的列数、页面框架和列框架设置。
    5. 你将循环遍历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)
    
    
    1. 如果列索引除以每页的列数等于0,从而表明该列是其页面上的第一个列,则创建一个新的页面视图来保存这些列。要设置它的框架,取边距设置。pag竖立和偏移其x原点的当前页面索引乘以屏幕的宽度;因此,在分页滚动视图中,每个杂志页面都将在前一个页面的右侧。
    2. 增加pageIndex。
    3. 通过设置划分页面视图的宽度。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
    
    1. 创建一个CGMutablePath列的大小,然后从textPos开始,呈现一个新的CTFrame与尽可能多的文本。
    2. 创建一个CTColumnView与CGRect columnFrame和CTFrame CTFrame然后添加列pageView。
    3. 使用ctframegetvisiblestringgrange(_:)计算列中包含的文本范围,然后将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上构建并运行应用程序。检查双列布局!左右拖动可以在页面之间切换。看起来似乎不错 😄

    image

    您有列和格式化的文本,但您缺少图像。用Core Text绘制图像并不是那么简单——毕竟它是一个文本框架——但是在你已经创建的标记解析器的帮助下,添加图像应该不会太糟糕。

    在核心文本绘制图像

    虽然Core Text不能绘制图像,但作为一个布局引擎,它可以为图像留出空间。通过设置CTRun的委托,可以确定CTRun的上升空间、下降空间和宽度。像这样:


    image

    当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)
        }
      }
    }
    
    1. 如果标签以"img"开头,使用regex搜索图像的"src"值,即文件名。
    2. 将图像宽度设置为列的宽度,并设置其高度,以便图像保持其高度-宽度宽高比。
    3. 如果图像的高度对于列来说太长,则设置高度以适应列,并减少宽度以保持图像的长宽比。由于图像后面的文本将包含空格属性,所以包含空格信息的文本必须适合于图像的同一列;因此将图像高度设置为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))
    
    1. 附加一个包含图像大小,文件名和文本位置的Dictonary到图像。
    2. 定义RunStruct来保存描述空白的属性。然后初始化一个指针,使其包含一个RunStruct,其上升高度等于图像高度,宽度属性等于图像宽度。
    3. 创建一个CTRunDelegateCallbacks,返回属于RunStruct类型指针的上升、下降和宽度属性。
    4. 使用ctrundelegateccreate创建一个绑定回调和数据参数的委托实例。
    5. 创建一个包含委托实例的带属性字典,然后向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 {
    
            }
        }
      }
    }
    
    1. 获取CTFrame的CTLine对象的数组。
    2. 使用CTFrameGetOrigins将ctframe的行原点复制到origin数组中。通过设置一个长度为0的范围,CTFrameGetOrigins将知道要遍历整个CTFrame。
    3. 设置nextImage以包含当前图像的属性数据。如果nextImage包含图像的位置,展开它并继续;否则,提前返回。
    4. 循环文本行。
    5. 如果该行的字形运行,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
    }
    
    1. 如果当前运行的范围不包含下一个图像,则跳过循环的其余部分。否则,在这里渲染图像。
    2. 使用CTRunGetTypographicBounds计算图像宽度,并将高度设置为找到的上升高度。
    3. 使用CTLineGetOffsetForStringIndex获取直线的x偏移量,然后将其添加到imgBounds的原点。
    4. 将图像及其帧添加到当前的CTColumnView中。
    5. 增加图像索引。如果有一个图像在images[imageIndex],更新nextImage和imgLocation,以便它们引用下一个图像。
    image

    好的!太棒了!差不多了,最后一步。

    添加以下右边的pageView.addSubview(列)内的buildFrames(withAttrString:and imagages:)附加图像,如果他们存在:

    if images.count > imageIndex {
      attachImagesWithFrame(images, ctframe: ctframe, margin: settings.margin, columnView: column)
    }
    

    看看iphone 和ipad的效果:


    image

    恭喜!为了感谢你们的辛勤工作,僵尸们放过了你们的大脑!:]

    哪里可以看到效果?

    在这里查看完成的项目

    正如在介绍中提到的,Text Kit通常可以替代Core Text;所以试着用Text Kit编写这个教程,看看它是如何比较的。也就是说,这堂 Core Text课不会是徒劳的!TextKit提供了免费的桥接到CoreText,所以你有需要的话,可以很容易地进行框架之间的转换。

    相关文章

      网友评论

          本文标题:翻译 - Core Text Tutorial for iOS:

          本文链接:https://www.haomeiwen.com/subject/kdnxkrtx.html