iOS精进iOS面试

Core Text框架详细解析(七) —— 基于Core Tex

2018-08-27  本文已影响35人  刀客传奇

版本记录

版本号 时间
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 :),设置:

接下来,创建一个名为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)
  }
}

打开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 {
    }
  }
}

是时候开始循环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)

接下来,在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

最后在循环后设置滚动视图的内容大小:

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)
    }
  }
}

接下来,在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))

现在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 {

        }
    }
  }
}

接下来,在字形运行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
}

到最后一步了。

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

上一篇下一篇

猜你喜欢

热点阅读