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字体操作

开始

首先看一下本文的写作环境:

Swift 4, iOS 11, Xcode 9

Core Text是一个低级底层文本引擎,与Core Graphics / Quartz框架一起使用时,可以对布局和格式进行细粒度控制。

在iOS 7中,Apple发布了一个名为Text Kit的高级库,它存储,布局和显示具有各种排版特征的文本。尽管Text Kit功能强大且通常在布局文本时足够,但Core Text可以提供更多控制。例如,如果您需要直接使用Quartz,请使用Core Text。如果您需要构建自己的布局引擎,Core Text将帮助您生成“glyphs and position them relative to each other with all the features of fine typesetting.”

本教程将指导您使用Core Text创建一个非常简单的杂志应用程序!

下面就新建立一个工程并开始我们的工作。

打开Xcode,使用Single View Application Template创建一个新的Swift universal project,并将其命名为CoreTextMagazine

接下来,将Core Text框架添加到项目中:

  • 1)单击项目导航器中的项目文件(左侧的条带)
  • 2)在“General”下,向下滚动到底部的“Linked Frameworks and Libraries”
  • 3)点击“+”并搜索“CoreText”
  • 4)选择“CoreText.framework”并单击“Add”按钮。 而已!

现在项目已经设置好,是时候开始编码了。


Adding a Core Text View - 添加一个Core Text视图

对于初学者,您将创建一个自定义UIView,它将在其draw(_ :)方法中使用Core Text

创建一个名为CTView继承自UIView的新Cocoa Touch类文件。

打开CTView.swift,并在导入UIKit下添加以下内容:

import CoreText

接下来,将此新自定义视图设置为应用程序中的主视图。 打开Main.storyboard,打开右侧的Utilities菜单,然后在其顶部工具栏中选择Identity Inspector图标。 在Interface Builder的左侧菜单中,选择View。 Utilities菜单的Class字段现在应该是UIView。 要子类化主视图控制器的视图,请在“Class”字段中键入CTView,然后按Enter键。

接下来,打开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)
}

下面,让我们一步一步地回顾一下:

  • 1)创建视图后,draw(_ :)将自动运行以渲染视图的背景层。
  • 2)展开您将用于绘图的当前图形上下文。
  • 3)创建一个限制绘图区域的路径,在这种情况下是整个视图的边界
  • 4)在Core Text中,使用NSAttributedString(而不是String或NSString)来保存文本及其属性。 将“Hello World”初始化为属性字符串。
  • 5)CTFramesetterCreateWithAttributedString使用提供的属性字符串创建CTFramesetterCTFramesetter将管理您的字体引用和绘图框架。
  • 6)通过让CTFramesetterCreateFrame呈现path中的整个字符串来创建CTFrame
  • 7)CTFrameDraw在给定的上下文中绘制CTFrame

这就是你需要绘制一些简单的文字! Build,运行并查看结果。

呃哦......那似乎不对,是吗? 与许多低级API一样,Core Text使用Y-flipped坐标系。 更糟糕的是,内容也垂直翻转!

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并运行应用程序。 不要担心状态栏重叠,您将学习如何使用边距修复此问题。

祝贺您的第一个Core Text应用程序!


The Core Text Object Model - Core Text 对象模型

如果你对CTFramesetterCTFrame感到有点困惑 - 这没关系,因为是时候说明一下了。

以下是Core Text对象模型的样子:

创建CTFramesetter引用并为其提供NSAttributedString时,会自动为您创建CTTypesetter实例以管理您的字体。 接下来,使用CTFramesetter创建一个或多个frames,您将在其中呈现文本。

创建frame时,为其提供要在其矩形内渲染的文本子范围。 Core Text会自动为每行文本创建一个CTLine,并为具有相同格式的每段文本创建一个CTRun。 例如,如果您将多个单词连续显示为红色,则会创建一个CTRun,然后是另一个CTRun用于以下纯文本,然后是另一个用于粗体句子的CTRun等。Core Text根据提供的NSAttributedString属性为您创建CTRuns。 此外,这些CTRun对象中的每一个都可以采用不同的属性,因此您可以很好地控制字距,连字,宽度,高度等。


Onto the Magazine App!

下载并取消归档 the zombie magazine materials

将文件夹拖到Xcode项目中。 出现提示时,确保选中Copy items if neededCreate groups

要创建应用程序,您需要将各种属性应用于文本。 您将创建一个简单的文本标记解析器,它将使用标签来设置杂志的格式。

创建一个名为MarkupParser的新Cocoa Touch类文件,继承自NSObject。

首先,快速浏览一下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)搜索标记的整个范围以进行regex匹配,然后生成结果NSTextCheckingResult的数组。

现在,您已将所有文本和格式标记解析为chunks,您将循环遍历chunks以构建属性字符串。

但在此之前,您是否注意到matches(in:options:range:)如何接受NSRange作为参数? 将NSRegularExpression函数应用于标记String时,会有很多NSRangeRange转换。

仍然在MarkupParser.swift中,将以下extension添加到文件末尾:

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

此函数将String的起始和结束索引(由NSRange表示)转换为String.UTF16View.Index格式,即字符串的UTF-16代码单元集合中的位置;然后将每个String.UTF16View.Index转换为String.Index格式;组合时,产生Swift的范围格式:Range。 只要索引有效,该方法将返回原始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)
}
  • 1)遍历chunks
  • 2)获取当前NSTextCheckingResult的范围,打开Range <String.Index>并继续块,只要它存在。
  • 3)将chunk拆分为由“<”分隔的部分。 第一部分包含杂志文本,第二部分包含标签(如果存在)。
  • 4)使用fontName创建字体,默认情况下为“Arial”,相对于设备屏幕的大小。 如果fontName未生成有效的UIFont,请将font设置为默认字体。
  • 5)创建字体格式的字典,将其应用于parts [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)如果少于两个部分,则跳过循环体的其余部分。否则,将第二部分存储为tag
  • 2)如果tag“font”开头,则创建一个正则表达式以查找字体的“ color”值,然后使用该正则表达式来枚举tag的匹配“ color”值。在这种情况下,应该只有一个匹配的颜色值。
  • 3)如果enumerateMatches(in:options:range:using :)返回tag中有效范围的有效match,请找到指示的值(例如<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”)

在这里,您创建了一个实例变量来保存attributed string,以及一种从应用程序的其他位置设置它的方法。

接下来,打开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文件中的文本加载到String中。
  • 2)创建一个新的解析器,输入文本,然后将返回的属性字符串传递给ViewControllerCTView

Build并运行应用程序!

棒极了? 感谢大约50行解析,您只需使用文本文件来保存杂志应用程序的内容。

后记

本篇主要讲述了基于Core Text的Magazine App的制作,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容