Core Text框架详细解析(七) —— 基于Core Text的Magazine App的制作(二)

版本记录

版本号 时间
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(_ :)中创建了framesetterframe,但由于您将拥有许多具有不同格式的列,因此最好创建单独的列实例。

创建一个名为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)CTFramesetterframesetter将创建属性文本的每个列的CTFrame
  • 4)UIViewpageViews将作为每个页面的列子视图的容器,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 columnFrameCTFrame 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类型指针的ascentdescentwidth属性。
  • 4)使用CTRunDelegateCreate创建一个代理实例,将回调和数据参数绑定在一起。
  • 5)创建一个包含代理实例的属性字典,然后在attrString中附加一个空格,该空格保存文本中孔的位置和大小调整信息。

现在MarkupParser正在处理“img”标签,你需要调整CTColumnViewCTView来渲染它们。

打开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)获取一组ctframeCTLine对象。
  • 2)使用CTFrameGetOriginsctframe的行原点复制到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]上有图像,请更新nextImageimgLocation,以便它们引用下一个图像。

到最后一步了。

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的制作,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容