无标题文章,制作一个简单图文混排的杂志

2019-09-14 16:34栏目:专项工作
TAG:

学习CoreText入门时发现的文章,特此翻译下来,以资他人学习之用 原文链接 :

问题答案

我当时刚学iOS开发的时候一样的感觉 总想知道原理 内部怎么回事 感觉在像在雾里

Core Text是一个可以结合Core Graphics/Quartz框架的底层文本渲染引擎,可以提供精细的布局和格式化控制在iOS7,苹果发布了高层级的Text Kit库,它可以存储,列出并显示具有各种排版特征的文本。尽管Text Kit非常强大,也满足日常文本排版的需要,但是Core Text可以提供更精细的控制。例如,如果你需要直接使用Quartz框架,可以使用Core Text, 如果你需要打造自己独有的排版引擎,Core Text能帮你实现 对字体与相对位置相关的特征,进行精细的控制

在用UIDatePicker做一个倒计时选择时间的时候,碰到了这个问题:在datePIcker显示后,第一次滑动时,发现添加的事件并没有相应,第二次就可以正常相应了。

ag真人 1iOS学习交流群: 626433463

此教程将使用Core Text从0到1创建一个简单的杂志应用,那么开始吧

于是乎,去搜索了问题,最终在这里找到了答案,说是datePicker设置在 CountDownTimer mode 模式下,就会出现这样的bug.

但是iOS开发就是这样 他是封闭的 本身就是在雾里...

热身

  • 打开Xcode新建一个swift项目,命名为CoreTextMagazine

    ag真人 2新建项目

  • CoreText.framework导入工程

    ag真人 3导入CoreText.framework

  • 添加 Core Text View创建CTView.swift, 复写 draw方法

override func draw(_ rect: CGRect) { // 获取当前上下文 guard let context = UIGraphicsGetCurrentContext() else { return; } //转换成uikit坐标系 context.textMatrix = .identity context.translateBy(x: 0, y: rect.height) context.scaleBy(x: 1, y: -1) // 绘制区域路径 let path = CGMutablePath.init() path.addRect // 初始化富文本 let attString = NSAttributedString.init(string: "hello word") // 创建 CTFramesetter let frameSetter = CTFramesetterCreateWithAttributedString(attString as CFAttributedString) // 创建 CTFrame let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attString.length), path, nil) // 在指定上下文绘制CTFrame CTFrameDraw(frame, context)}

由于Quartz坐标系与UIKit坐标系略有不同,所以需要对坐标系进行一次转换,否则绘制出来的文本将会是倒置的

context.textMatrix = .identitycontext.translateBy(x: 0, y: rect.height)context.scaleBy(x: 1, y: -1)

ag真人,项目跑一下, 成功渲染

ag真人 4渲染文本

  • CoreText 对象模型CTFramesetter是啥?CTFrame又是啥?祭出此图ag真人 5CoreText对象模型

当你创建了一个CTFramesetter,并且为它提供了一个NSAttributedString,将会自动创建一个CTTypesetter来管理字体,接下来就可以使用CTFramesetter创建一个或者多个CTFrame来渲染文本

创建CTFrame的时候,CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, attString.length), path, nil)我们给定了范围和文本的绘制路径范围,Core Text自动为每一行文本创建了一个CTLine,每一块文本创建了一个CTRun, 每个CTRun能够设置不同的属性,例如你可以创建一个红色字体的CTRun,再创建另一个为粗体的CTRun,这就是为什么说Core Text能对文本进行精细控制的原因了

怎么样解决:

关于iOS开发的学习 打个比方就像把汽车分解

撸起袖子,开干

请下载素材链接:百度云链接: 解压之后导入工程即可

我们需要对不同的文本设置不同的属性,我们需要新建一个文本修饰格式解析器来解析 zombies.txt中的属性标签来格式化文本

  • 新建MarkupParser.swift 继承 NSObject我们首先可以粗看下zombies.txt中内容

ag真人 6zombies.txt

img src标签引用了图片,font color/face标签决定了文本的颜色和字体

MarkupParser.swift键入如下代码

// MARK: - 属性var color: UIColor = .blackvar fontName: String = "Arial"var attrString: NSMutableAttributedString!var images: [[String: Any]] = [] // MARK: - 初始化方法override init() { super.init()} // MARK: - 内部方法,解析html文本func parseMarkup(_ markup: String) { }

类中带有字体颜色,字体等属性,parseMarkup将从文本中解析成属性文本

对于以下文本

These are <font color="red">red<font color="black"> and<font color="blue">blue <font color="black">words.

将被解析渲染成如下样式

ag真人 7

  • 解析标签将如下代码填入parseMarkup方法
// attrString初始化为空,最终会被赋值最终解析结果attrString = NSMutableAttributedString(string: "")// 解析do { // 匹配标签块 let regex = try NSRegularExpression.init(pattern: "(<[^>]+>|\Z)", options: NSRegularExpression.Options.dotMatchesLineSeparators) let chunks = regex.matches(in: markup, options: NSRegularExpression.MatchingOptions.init(rawValue: 0), range: NSRange.init(location: 0, length: markup.count))} catch _ {}

正则匹配出所有的标签块

ag真人 8现在所有的匹配结果都在chunks变量中,只需要遍历chunks来创建AttributedString即可

在此之前,我们注意到matches(in:options:range:)接受了一个NSRange类型参数,接下来还会有许多用到NSRange 转换成 Range的地方,添加如下代码,可将 NSRange 转换成 Range:

// MARK: - String NSRange 转换成 Rangeextension 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 }}

parseMarkup 继续添加如下代码

// 设定默认字体let defaultFont: UIFont = .systemFont(ofSize: UIScreen.main.bounds.size.height / 40)// 遍历匹配结果for chunk in chunks { // 获取当前匹配结果 NSTextCheckingResult 在原文本中的范围 guard let markupRange = markup.range(from: chunk.range) else { continue } // 以符号 "<" 分割句子 let parts = markup[markupRange].components(separatedBy: "<") // 从 fontName 属性创建字体, 若无该字体,则使用默认字体 defaultFont let font = UIFont(name: fontName, size: UIScreen.main.bounds.size.height / 40) ?? defaultFont // 为 NSAttributedString 创建 字体颜色和字体 属性 let attrs = [NSAttributedStringKey.foregroundColor: color, NSAttributedStringKey.font: font] as [NSAttributedStringKey : Any] // 将属性 应用于 parts[0] let text = NSMutableAttributedString(string: parts[0], attributes: attrs) attrString.append}

为了解析处理font标签,继续添加如下代码

// 如果分割后的模式数组长度小于等于1,则略过 说明不带有形如 <> 的匹配if parts.count <= 1 { continue}let tag = parts[1]// 如果 parts[1] ( < 之后的文本,也就是标签名) 是 fontif tag.hasPrefix { // 匹配颜色属性 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 // 利用 NSObject perform 方法对 color 属性 赋值获取到的颜色 if let match = match, let range = tag.range(from: match.range) { let colorSel = NSSelectorFromString(tag[range]+"Color") color = UIColor.perform.takeRetainedValue() as? UIColor ?? .black } } // 正则匹配 face 字体属性 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

到现在为止,已经能够解析出 NSAttributedString

在我们的CTView.swift中添加

// MARK: - Propertiesvar attrString: NSAttributedString!// MARK: - Internalfunc importAttrString(_ attrString: NSAttributedString) { self.attrString = attrString}

然后 draw(_ rect: CGRect)中删除let attrString = NSAttributedString(string: "Hello World") from draw

ViewController.swift中设置入口

let ctView = CTView()ctView.frame = view.frameview.addSubviewguard let file = Bundle.main.path(forResource: "zombies", ofType: "txt") else { return }do { let text = try String(contentsOfFile: file, encoding: .utf8) // 解析器解析 let parser = MarkupParser() parser.parseMarkup ctView.importAttrString(parser.attrString)} catch _ {}

运行一下,Cool, 效果如下

ag真人 9image.png

 dispatch_async(dispatch_get_main_queue(), ^{ self.datePicker.countDownDuration = 0; });

最底层的原料有塑料 钢铁

杂志布局

我们不仅仅满足一个只显示单页面的应用,CTFrameGetVisibleStringRange使我们能控制一个frame中能显示多少文字,你可以创建列,显示满了之后,可以再次创建一列在这个应用中,我们将以列为单位,构建多个页面,最终构成一个杂志APP

解决思路,就是手动的调用一次 value changed

再用这些底层的东西造出来发动机 座椅

Let us down

我们先将CTView.swift中基类换成UIScrollView, 使App能够支持多页滚动

class CTView: UIScrollView {

到目前为止我们在CTView.swift中创建了一个framesetter,生成了并绘制了一个CTFrame接下来创建一个新的类CTColumnView.swift继承于UIView

class CTColumnView: UIView { var ctFrame: CTFrame! 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: - override func draw(_ rect: CGRect) { guard let context = UIGraphicsGetCurrentContext() else { return } // 转换成UIKit坐标系 context.textMatrix = .identity context.translateBy(x: 0, y: bounds.size.height) context.scaleBy(x: 1.0, y: -1.0) // 在上下文中绘制 CTFrame CTFrameDraw(ctFrame, context) }}

接下来我们需要一个CTSettings.swift来对Column列进行配置

class CTSettings { // MARK: - 属性 let margin: CGFloat = 20 // 边距 var columnsPerPage: CGFloat! // 每页列数 var pageRect: CGRect! // 页面大小 var columnRect: CGRect! // 列大小 // MARK: - 初始化 init() { // 如果是iphone 每页显示1列,否则每页两列 columnsPerPage = UIDevice.current.userInterfaceIdiom == .phone ? 1 : 2 // 页面frame 边距设置为 margin大小 pageRect = UIScreen.main.bounds.insetBy(dx: margin, dy: margin) // 设置列的frame columnRect = CGRect(x: 0, y: 0, width: pageRect.width / columnsPerPage, height: pageRect.height).insetBy(dx: margin, dy: margin) }}

打开CTView.swift, 删去已有代码, 添加如下代码

class CTView: UIScrollView {func buildFrames(withAttrString attrString: NSAttributedString, andImages images: [[String: Any]]) { // 允许UIScrollview 翻页进行翻动 isPagingEnabled = true // CTFrameSetter 将创建每列对应的 CTFrame let framesetter = CTFramesetterCreateWithAttributedString(attrString as CFAttributedString) // 属性 var pageView = UIView() var textPos = 0 //当前字符所在位置 var columnIndex: CGFloat = 0 //当前列下标 var pageIndex: CGFloat = 0 //当前页面下标 let settings = CTSettings() //配置 // 循环遍历,生成列 while textPos < attrString.length {

继续向遍历,生成列的循环代码添加:

// columnIndex %s ettings.columnsPerPage为零(truncatingRemainder:对浮点数取余),说明为页面第一列,需要新建一个页,并设置frame if columnIndex.truncatingRemainder(dividingBy: settings.columnsPerPage) == 0 { columnIndex = 0 pageView = UIView(frame: settings.pageRect.offsetBy(dx: pageIndex * bounds.width, dy: 0)) addSubview // 页面索引自增 pageIndex += 1 } // 列宽度 let columnXOrigin = pageView.frame.size.width / settings.columnsPerPage // 列偏移量 let columnOffset = columnIndex * columnXOrigin // 计算列的frame let columnFrame = settings.columnRect.offsetBy(dx: columnOffset, dy: 0) // 创建位置路径,确定text分绘制范围 let path = CGMutablePath() path.addRect(CGRect(origin: .zero, size: columnFrame.size)) // 创建 CTFramesetter 用来创建 CTFrame let ctframe = CTFramesetterCreateFrame(framesetter, CFRangeMake(textPos, 0), path, nil) // 创建列视图 let column = CTColumnView(frame: columnFrame, ctframe: ctframe) pageView.addSubview // 获取CTFrame 能容纳多少文本,从而更新textPos let frameRange = CTFrameGetVisibleStringRange textPos += frameRange.length // 列数指针自增 columnIndex += 1}

上述代码中,确定和计算出了每一列视图的位置和应该显示的文本范围最后,代码末尾,只需要重新设定下UIScrollViewcontentSize即可

// 更新UIScrollview的contentSizecontentSize = CGSize(width: CGFloat(pageIndex) * bounds.size.width, height: bounds.size.height)

ViewController中调用ctView.buildFrames即可

let text = try String(contentsOfFile: file, encoding: .utf8)let parser = MarkupParser()parser.parseMarkup//ctView.importAttrString(parser.attrString)ctView.buildFrames(withAttrString: parser.attrString, andImages: parser.images)

运行一下,wonderful,一个可翻页效果的App就有了

ag真人 10预览

这样就可以解决datePicker在第一次选择时,事件不响应bug

最后再加上写螺丝 胶水等 把汽车就拼起来了

为App渲染图片

尽管Core text无法直接绘制图片,但是它可以为图片预留显示空间 ,通过CTRun的代理CTRunDelegate,我们可以设定CTRun的上升下降高度,和它的宽度,模型如下图所示

ag真人 11CTRunDelegate

每当Core Text遇到一个CTRun,它就会询问代理我需要为这数据块预留多少空间?,通过CTRunDelegate,我们就能为图片显示预留出空间了

首先在MarkupParser.swift中添加解析img标签的代码

// image 数组 添加 图片属性字典images += [["width": NSNumber(value: Float, "height": NSNumber(value: Float, "filename": filename, "location": NSNumber(value: attrString.length)]]// 定义CTRun属性结构体struct RunStruct { let ascent: CGFloat let descent: CGFloat let width: CGFloat}// Memory指针 相当于RunStruct 结构体指针let extentBuffer = UnsafeMutablePointer<RunStruct>.allocate(capacity: 1)extentBuffer.initialize(to: RunStruct(ascent: height, descent: 0, width: width))// 创建CTRunDelegateCallbacks 控制占位var callbacks = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: {  in}, getAscent: {  -> CGFloat in let d = pointer.assumingMemoryBound(to: RunStruct.self) return d.pointee.ascent}, getDescent: {  -> CGFloat in let d = pointer.assumingMemoryBound(to: RunStruct.self) return d.pointee.descent}, getWidth: {  -> CGFloat in let d = pointer.assumingMemoryBound(to: RunStruct.self) return d.pointee.width})// 创建绑定了回调的代理let delegate = CTRunDelegateCreate(&callbacks, extentBuffer)// 将代理封装至属性字典let attrDictionaryDelegate = [(kCTRunDelegateAttributeName as NSAttributedStringKey): (delegate as Any)]attrString.append(NSAttributedString(string: " ", attributes: attrDictionaryDelegate))}

现在MarkupParser可以解析处理img标签了,现在我们只需让CTColumnView && CTView绘制出来就行了

对于CTColumnView.swift添加属性

var images: [(image: UIImage, frame: CGRect)] = []

并在draw(_ rect: CGRect)中添加绘制图片代码

// 绘制图片for imageData in images { if let image = imageData.image.cgImage { let imgBounds = imageData.frame context.draw(image, in: imgBounds) }}

CTView.swift中添加属性

var imageIndex: Int!

并且在buildFrames(withAttrString:andImages:):方法中做初始化

imageIndex = 0

再次添加attachImagesWithFrame(_:ctframe:margin:columnView)方法

func attachImagesWithFrame(_ images: [[String: Any]], ctframe: CTFrame, margin: CGFloat, columnView: CTColumnView) { // 获取ctframe 的`CTLine`数组 let lines = CTFrameGetLines as NSArray // 使用CTFrameGetLineOrigins 将ctframe中的行origin 复制到数组 origins var origins = [CGPoint](repeating: .zero, count: lines.count) // CFRangeMake代表转换整个CTFrame CTFrameGetLineOrigins(ctframe, CFRangeMake, &origins) // 获取图片对象的location属性,如果没有值直接返回 var nextImage = images[imageIndex] guard var imgLocation = nextImage["location"] as? Int else { return } // 遍历CTLine for lineIndex in 0..<lines.count { }}

继续在循环中添加代码

// 遍历CTLinefor lineIndex in 0..<lines.count { let line = lines[lineIndex] as! CTLine // 如果CTRun, 文件名,图片都存在 if let glyphRuns = CTLineGetGlyphRuns as? [CTRun], let imageFilename = nextImage["filename"] as? String, let img = UIImage(named: imageFilename) { for run in glyphRuns { // 如果当前CTRun的范围range没有包含nextImage,直接进入一下循环 let runRange = CTRunGetStringRange if runRange.location > imgLocation || runRange.location + runRange.length <= imgLocation { continue } // 通过 CTRunGetTypographicBounds 计算图片的大小 var imgBounds: CGRect = .zero var ascent: CGFloat = 0 imgBounds.size.width = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake, &ascent, nil, nil)) imgBounds.size.height = ascent // 通过 CTLineGetOffsetForStringIndex 计算 CTLine x轴的偏移量, let xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange.location, nil) // 偏移量需要加上 imgBounds 的 origin imgBounds.origin.x = origins[lineIndex].x + xOffset imgBounds.origin.y = origins[lineIndex].y // 将image 和 image绘制的位置 加入 columnView columnView.images += [(image: img, frame: imgBounds)] // 图片下标自增,更新imgLocation imageIndex! += 1 if imageIndex < images.count { nextImage = images[imageIndex] imgLocation = (nextImage["location"] as AnyObject).intValue } } }}

最终,在buildFrames(withAttrString:andImages:)方法中,语句pageView.addSubview之前调用即可

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

大功告成

ag真人 12APP

github完整源码地址 传送门: 给个start呗

iOS基本都是英文的资料 也由于封闭 文档写的相当好

在遇到新框架的时候

弄明白框架的功能

去文档里搜搜 框架的 Programming Guide 很有用

要弄明白框架类的继承结构

写iOS的程序不一定都是用OBJC 很多框架是用C写的

学习iOS开发基础可以按照下面两个方面学

基础

OBJ-C --- 语法弄明白 @interface @property 这些东西总要知道是干嘛的 怎么用

基础库 --- NSString NSArray NSDictionary等 这些东西在所有的框架里都会出现

iOS大部分类都是继承自NSObject (我还没见过不是继承自NSObject的..)

还有一些 像NSCopying的接口(经@李禹龙提醒 应该叫协议) 不是特别用到开始不用了解

NSObject 创建对象的时候用 + alloc 方法 创建后需要init方法初始化 这个init指的是所有前面是init的方法比如UIView的初始化方法是 - initWithFrame:aRect 在Objc里有很多这样关于函数命名的约定 类似于在python中的函数__xxx

NSString 字符串 NSArray 数组 NSDictionary 字典 这些都需要弄很清楚 其他的类都是一个套路

NSMutableArray 这样带Mutable的类代表可变的 继承自相应的不可变类 比如NSMutableArray继承自NSArray 他们都添加了可以改变对象内容的方法比如

- addObject:anObject 添加对象

- removeObject:anObject 删除对象

上面只是一个大概的总结 还有很多东西需要学 iOS5的SDK已经支持ARC 可以自动进行release 但是对iOS4的支持还有一个小问题 现在要开发应用 可能还需要按照之前的MRC的方式alloc release retain autorelease 之类的内存管理方法 不过如果你现在开始学 到编出像样的APP iOS5可能已经普及了 可以直接用ARC (另 之前对ARC的了解很粗浅 现在开发程序完全可以直接ARC iOS4不支持的weak是有办法替代的 用unsafe_unretained 如果同时支持iOS5和iOS4 用宏判断下就可以 当然也可以直接用assign)

还有一点开始学习的时候肯定很疑惑 内存管理是基于函数名称的 比如带alloc copy的函数 用了之后返回的对象一定要release 这个不用疑惑 照做就行了

高级库

版权声明:本文由ag真人发布于专项工作,转载请注明出处:无标题文章,制作一个简单图文混排的杂志