美文网首页swiftiOS Dev收藏ios
Swift: 基于 CoreText 图文排版实践

Swift: 基于 CoreText 图文排版实践

作者: 伊尔今夏 | 来源:发表于2018-01-06 20:36 被阅读83次

    介绍 CoreText 简单应用,主要包括文本节选,可点链接,图文混排等内容。

    CoreText

    CoreText 是用于处理文字和字体的底层技术。它直接和 Core Graphics 交互。

    Coretext

    CoreText 对象能直接获取文本的宽高信息,占用内存少,异步绘制等特点。在引起 UITableView 卡顿常见的原因 Cell 层级过多,离屏渲染,频繁计算 Cell 高度等耗时操作。这个时候 CoreText 就派上用场了,减少层级,CoreText 可以直接将文字和图片直接绘制在 Layer 上,并且支持异步绘制大大节约主线程资源。用来做图文混排的 UITableView 的优化,效果很明显。

    基础概念

    Font & Character & Glyphs

    Font 在计算机意义上字体表示的是同一个大小,同一样式字形的集合

    Character 字符表示信息本身,字形是它的图形表示形式,字符一般指某种编码,比如 Unicode 编码就是其中一种。字符和字形不是一一对应关系,同一个 Character 不同 Font 会生成不同的 Glyphs

    Glyphs 字形常见参数

    字形
    • Baseline : 参照线,是一条横线,一般为此为基础进行字体的渲染
    • Leading : 行与行之间的间距
    • Kerning : 字与字之间的间距
    • Origin : 基线上最左侧的点
    • Ascent : 一个字形最高点到基线的距离
    • Decent : 一个自行最低处到基线的距离,所以一个字符的高度是 ascent + decent 。当一行内有不同字体的文字时候,取最大值 max(ascent + decent)。
    • Line Height : max(ascent + decent) + Leading

    富文本NSAttributedString

    iOS 中用于描述富文本的类,它比 String 多了很多描述字体的属性,.font.underlineColor.foregroundColor 等,而且可以设定属性对应的区域 NSRange

      let text: NSMutableAttributedString = NSMutableAttributedString(string: "test")
    
      let attributes: [NSAttributedStringKey: Any] = [.font: UIFont.systemFontSize, .foregroundColor: UIColor.black, .underlineColor: UIColor.blue]
    
      text.addAttributes(attributes, range: NSMakeRange(0, 1))
      
    
    屏幕快照 2018-01-09 下午5.12.30.png

    在绘制过程中,其中 CTFramesetter 是由 CFAttributedString(NSAttributedString) 初始化而来,通过传入 CGPath 生成相应的 CTFrame 最后渲染到屏幕是 CTFrame

    
      let frameSetter: CTFramesetter = CTFramesetterCreateWithAttributedString(text)
    
      let path = UIBezierPath(rect: CGRect())
    
      let frame: CTFrame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path.cgPath, nil)
            
    

    一个 CTFrame 由一个或者多个 CTLine 组成,一个 CTLine 由一个或者多个 CTRun 组成。一个 CTRun 是由相同属性的字符组成。

    override func draw(_ rect: CGRect) {
    
       guard let context = UIGraphicsGetCurrentContext() { return }
       ...
    
       CTFrameDraw(ctFrame, context)
       
       ...
       let lines = CTFrameGetLines(frame) as! Array
       
       ...
       let runs = CTLineGetGlyphRuns(lines[0] as! CTLine) 
       
       ...         
    }
    
    

    产品需求是做一个类似知乎的问答系统,支持图文,标签,链接,短视频等基本元素。本文主要介绍基于 CoreText 图文排版一些简单实践应用。

    UIKit & CoreText

    文本

    直接看代码吧,简单输出一段文字。

    
    // BKCoreTextConfig.swift
    
    // 文本配置信息
    
    struct BKCoreTextConfig {
    
       let width  : CGFloat // 文本最大宽度
    
       let fontName : String  
    
       let fontSize : CGFloat
    
       let lineSpace : CGFloat // 行间距
    
       let textColor : UIColor
    
       init(width: CGFloat, fontName: String, fontSize: CGFloat, 
    
       lineSpace: CGFloat, textColor: UIColor) {
    
           self.width = width
    
           self.fontName = fontName
    
           self.fontSize = fontSize
    
           self.lineSpace = lineSpace
    
           self.textColor = textColor
    
       }
    
    }
    
    
    
    // BKCoreTextData.swift
    
    // 绘制信息内容
    struct BKCoreTextData {
    
       let ctFrame : CTFrame
    
       let size  : CGSize
    
       init(ctFrame: CTFrame, contentSize: CGSize) {
    
           self.ctFrame = ctFrame
    
           self.size = contentSize
    
       }
    
    }
    
    
    
    // BKCoreTextParser.swift
    
    // 解析
    
    static func attributes(with config: BKCoreTextConfig) -> NSDictionary {
    
         // 字体大小
         let font = CTFontCreateWithName(config.fontName as CFString, config.fontSize, nil)
    
         //设置行间距
         var lineSpace = config.lineSpace
    
         let settings: [CTParagraphStyleSetting] =
    
         [CTParagraphStyleSetting(spec: CTParagraphStyleSpecifier.lineSpacingAdjustment, valueSize:       MemoryLayout<CGFloat>.size, value: &lineSpace),
    
         CTParagraphStyleSetting(spec: CTParagraphStyleSpecifier.maximumLineSpacing, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpace),
    
         CTParagraphStyleSetting(spec: CTParagraphStyleSpecifier.minimumLineSpacing, valueSize: MemoryLayout<CGFloat>.size, value: &lineSpace)]
    
         let paragaraph = CTParagraphStyleCreate(settings, 3)
    
         //设置字体颜色
         let textColor = config.textColor
    
         let dict = NSMutableDictionary()
    
         dict.setObject(font, forKey: kCTFontAttributeName as! NSCopying)
    
         dict.setObject(paragaraph, forKey: kCTParagraphStyleAttributeName as! NSCopying)
    
         dict.setObject(textColor.cgColor, forKey: kCTForegroundColorAttributeName as! NSCopying)
    
         return dict
    
    }
    
    static func createFrame(frameSetter: CTFramesetter, config: BKCoreTextConfig, height: CGFloat) -> CTFrame {
    
         let path = CGMutablePath()
    
         path.addRect(ccr(x: 0, y: 0, width: config.width, height: height))
    
         return CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)
    }
    
    static func parse(content: NSAttributedString, config: BKCoreTextConfig) -> BKCoreTextData {
    
         let frameSetter = CTFramesetterCreateWithAttributedString(content)
    
         let restrictSize = CGSize(width: config.width, height: CGFloat(MAXFLOAT))
    
         let coretextSize = CTFramesetterSuggestFrameSizeWithConstraints(frameSetter, 
    
         CFRangeMake(0, 0), nil, restrictSize, nil)
    
         let height = coretextSize.height
    
         let frame = self.createFrame(frameSetter: frameSetter, config: config, height: height)
    
         retutn BKCoreTextData.init(ctFrame: frame, contentSize: coretextSize)
    
    }
    
    static func handleText(text: String, config: BKCoreTextConfig) -> BKCoreTextData {
    
    /*
    
    let text = "裘德洛论颜值的话,绝对可以称得上帅的人神共愤,海洋般蓝绿交织的双眼,优雅俊美,随随便便一个动作都能俘获万千少女的心。而且人家不止有颜还多才多艺,小小年纪开始就在音乐剧团表演,气质啊才华啊什么的,完美的让人嫉妒。美图奉上↓"
    
    let config = BKCoreTextConfig(width: kScreenWidth - 30, fontName: "PingFangSC-Regular", fontSize: 12, textColor: UIColor(rgb: 0x9E9E9E))
    
    */
    
         let attributes = self.attributes(with: config) as? [NSAttributedStringKey : Any]
    
         let attributedString = NSMutableAttributedString(string: content, attributes: attributes)
    
         return parse(content: attributedString, config: config)
    
    }
    
    

    上面就是给定给一个文本和文本一些配置信息得到一个 frame 现在可以直接绘制出一段文本

    
    // BKCoreTextView.swift
    
    var data : BKCoreTextData? {
    
         didSet { setNeedsDisplay() }
    
    }
    
    ....
    
    override func draw(_ rect: CGRect) {
    
         super.draw(rect)
    
         guard let context = UIGraphicsGetCurrentContext(), let info = data else { return }
    
         /// !!! 坐标转换
         context.textMatrix = CGAffineTransform.identity
    
         context.translateBy(x: 0, y: bounds.size.height)
    
         context.scaleBy(x: 1, y: -1)
    
         CTFrameDraw(info.ctFrame, context)
    
    }
    
    

    CoreText 坐标系是以左下角为坐标原点,UIKit是以左上角为坐标原点,使用 Core Graphics 需要做坐标的转换,不然看到的内容是倒过来的。

    文本节选

    以上只是简单的绘制了一段文本,但是呢,产品有需求要限制文本的行数,超过的用 ... 来表示更多。类似于微信朋友圈,内容过多会有 收起 全部 的按钮,功能是相类似的。

    如果用 UILabel 设置宽高,UILabel 会自动帮我们处理 ...。但是 CoreText 需要开发组手动处理,这里主要问题就是要找到最后一行合适的位置放置 ...

    思路:

    • 文本通过相关配置文件转换为 NSAttributedString 格式

    • 计算文本的行数 Count

    • 文本行数 Count 小于指定行数 numberOfLines,返回无需处理

    • 文本行数大于指定行数,截取最后一行处理

    • 最后一行显示宽度小于 Config.width, 直接行尾添加 ...

    • 最后一行显示宽度大于等于 Config.width ,需对最后一行做 replace 操作

    难点: 如何获取最后一行显示宽度

    
    public func CTLineGetOffsetForStringIndex(_ line: CTLine, _ charIndex: CFIndex, _ secondaryOffset: UnsafeMutablePointer<CGFloat>?) -> CGFloat
    
    

    函数 CTLineGetOffsetForStringIndex 是获取一行文字中指定 charIndex 字符相对原点的偏移量,返回值与 secondaryOffset 同为一个值。如果 charIndex 超出一行的字符长度则返回最大长度结束位置的偏移量。因此想求一行字符所占的像素长度时,就可以使用此函数,将 charIndex 设置为大于字符长度即可这里设置了 100 但是感受算出来的长度还是有一丢丢误差。

    
    static func handleText(text: String, config: BKCoreTextConfig, numberofLines: Int) -> BKCoreTextData {
    
        let attributes = self.attributes(with: config) as? [NSAttributedStringKey : Any]
    
        let attributedString = NSMutableAttributedString(string: content, attributes: attributes)
    
        let currCoreData = self.parse(content: attributedString, config: config)
    
        let lines = CTFrameGetLines(currCoreData.ctFrame) as Array
    
        let count = lines.count
    
        guard numberofLines > 0 else { return currCoreData }
    
         guard count > 0 && count > numberofLines else { return currCoreData }
    
         let num = min(numberofLines, count)
    
         let line = lines[num-1]
    
         let range = CTLineGetStringRange(line as! CTLine)
    
         let position = range.location + range.length
    
         let tmpAttrString = attr.attributedSubstring(from: NSMakeRange(0, position))
    
         var newContent = NSAttributedString()
    
         var offset: CGFloat = 0
    
         CTLineGetOffsetForStringIndex(line as! CTLine,100,&offset)
    
         let length = offset > (config.width - 10) ? range.length - 3 : range.length
    
         let lastLine: NSMutableAttributedString = tmpAttrString.attributedSubstring(from: NSMakeRange(range.location, length)) as! NSMutableAttributedString
    
         /// !!! 去除最后一行的 \n 
         var str = (lastLine.mutableString.mutableCopy() as! String).replacingOccurrences(of: "\n", with: "")
    
         str.append("...")
    
         let tmp = tmpAttrString.attributedSubstring(from: NSMakeRange(0, range.location))
    
         let newAttr: NSAttributedString = tmp.appending(NSAttributedString.init(string: str))
    
         let attributes = self.attributes(with: config) as? [NSAttributedStringKey : Any]
    
         newContent = NSMutableAttributedString(string: newAttr.string, attributes: attributes)
    
         return self.parseContent(content: newContent, config: config)
    
    }
    
    
    文本节选

    可点链接

    可点链接也是很常见的,比如 点我跳转

    后台给的 JSON 字符串可能直接原生态甩过来

    
    print(content)
    
    "嘎嘎嘎嘎哈哈哈哈哈https://wapbaike.baidu.com/item/%e4%b8%9c%e4%ba%ac%e5%9b%bd%e9%99%85%e7%94%b5%e5%bd%b1%e8%8a%82/187783?fr=aladdin"
    
    

    ********思路:********

    • 扫描文本,找出链接地址的字符串 results

    • 文本通过相关配置文件 Config 转换为 attributedString

    • 超链接文本通过配置文件 Config 转换为 linkAttributedString

    • 遍历 resultslinkAttributedString 替换 attributedString 中链接地址

    • 记录 linkAttributedStringrangeurl 得到 coreTextLinkDatas

    
    // BKCoreTextData.swift
    
    /// 可点击链接
    
    struct BKCoreTextLinkData {
    
         let title  : String
    
         let url  : String
    
         let range  : NSRange
    
    }
    
    struct BKCoreTextData {
    
    ...
    
        var linkData : [BKCoreTextLinkData]?
    
    ...
    
    }
    
    
    
    static func handleText(text: String, config: BKCoreTextConfig, numberofLines: Int) -> BKCoreTextData {
    
        return self.handleLinkAttribute(content: content, config: config) { (attributedString, linkDatas) in
    
    // 同上
    
    ....
    
             let newCoreData =  self.parseContent(content: attributedString, config: config)
    
             if let links = linkDatas, links.count > 0 {
    
                newCoreData.linkData = links 
    
              }
    
             return newCoreData
         }
    
        static func handleLinkAttribute(content: String, config: BKCoreTextConfig, completed: @escaping
            ( _ result : NSAttributedString, _ linkDatas : [BKCoreTextLinkData]?) -> BKCoreTextData) -> BKCoreTextData {
    
            let dataDetector = try? NSDataDetector(types: NSTextCheckingTypes(NSTextCheckingResult.CheckingType.link.rawValue))
            let results = dataDetector?.matches(in: content, options: NSRegularExpression.MatchingOptions.reportProgress, range: NSMakeRange(0, content.length))
            
            let attributes = self.attributes(with: config) as? [NSAttributedStringKey : Any]
            let attributedString = NSMutableAttributedString(string: content, attributes: attributes)
            
            let linkAttributedString = IconCodes.attributedString(code: .link, size: config.link.fontSize, color: config.link.textColor).appending(NSAttributedString(string: " 网页链接", font: UIFont(name: config.link.fontName, size: config.link.fontSize)!, color: .app_light))
            
            var tmpLinkDatas = [BKCoreTextLinkData]()
            
            if let results = results {
                results.reversed().forEach({ (result) in
                    if result.resultType == .link, let url = URL(string: (content as NSString).substring(with: result.range)) {
                        linkAttributedString.addAttributes([.link: url], range: NSMakeRange(0, linkAttributedString.length))
                        attributedString.replaceCharacters(in: result.range, with: linkAttributedString)
                        let data = BKCoreTextLinkData.init(title: linkAttributedString.string, url: url.absoluteString, range: NSMakeRange(result.range.location, linkAttributedString.length))
                        tmpLinkDatas.append(data)
                    }
                })
            }
            return completed(attributedString, tmpLinkDatas)
        }
    }
    
    

    一个文本中可能有多个链接,需要识别链接,以及记下每个链接的 Range

    成果

    效果有了,点击事件要怎么响应,这个时候就用到了上述记录的链接对应的 Range

    遍历 coreTextLinkDatas ,找到在 range 中包含获取点击位置 pointcoreTextLinkData 拿到 url 地址

    
    // BKCoreTextView.swift
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    
         let touch = (touches as NSSet).anyObject() as! UITouch
    
         let point = touch.location(in: self)
    
         guard let frame = data?.ctFrame else { return }
    
         let lines = CTFrameGetLines(frame) as Array
    
         let count = lines.count
    
         var origins = [CGPoint].init(repeating: CGPoint(0, 0), count: count)
    
         CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins)
    
         var transform = CGAffineTransform.init(translationX: 0, y: bounds.height)
    
         transform = transform.scaledBy(x: 1, y: -1)
    
         guard let links = data?.linkData, links.count > 0 else {
    
         print("没有可点击链接")
    
         return
    
         }
    
          for (index, line) in lines.enumerated() {
    
             let origin = origins[index]
    
             let lineRect = getLineBound(line: line as! CTLine, point: origin)
    
             let rect = lineRect.applying(transform)
    
             if rect.contains(point) == true {
    
               let relativePoint = CGPoint(point.x - rect.minX, point.y - rect.minY)
    
               let idx = CTLineGetStringIndexForPosition(line as! CTLine, relativePoint)
    
               if let link = foundLinkData(at: idx), let url = URL.init(string: link.url) {
    
                 print("oh! 点到了。\(url)")
    
                 return
    
               } else {
    
                 print("不在点击链接范围")
    
             }
    
           }
    
       }
    
    }
    
    func getLineBound(line: CTLine, point: CGPoint) -> CGRect {
    
           var ascent: CGFloat = 0
    
           var descent: CGFloat = 0
    
           var leading: CGFloat = 0
    
           let width: CGFloat = CGFloat(CTLineGetTypographicBounds(line, &ascent, &descent, &leading))
    
           let height: CGFloat = ascent + descent
    
           return ccr(point.x, point.y - descent, width, height)
    
    }
    
    func foundLinkData(at index: Int) -> BKCoreTextLinkData? {
    
           var link : BKCoreTextLinkData?
    
           data?.linkData?.forEach( {
    
             if NSLocationInRange(index, $0.range ) == true {
    
               link = $0
    
             }
    
           })
    
         return link
    
    }
    
    

    这只是比较简单的链接样式,可以给它添加下划线,按压态等等,设置它的 NSAttributedString 样式就可以了。

    图片混排

    就是图片和文字混合排版,如果图片比较多文字少不建议用 Core Text

    Core Text 是一个文本处理框架,不能直接绘制图片,但是它可以给图片预留空间,结合Core Graphic 来绘图。

    单排

    思路

    • 根据 Config 图片的宽高,设置 CTRunDelegateCallbacks

    • 生成 runDelegate

    • 找到要插入图片的位置,将图片信息封装成一个 attributedString 富文本类型的占位符

    • 富文本类型的占位符

    
    struct BKCoreTextData {
    
         let ctFrame : CTFrame
    
         let size : CGSize
    
         let imageUrl: String
    
    }
    
    
    
    struct BKCoreTextConfig {
    
         let size : CGSize
    
    }
    
    
    
    static func parse(content: String, imageUrl: String, config: BKCoreTextConfig) -> BKCoreTextData {
    
         var imageCallback = CTRunDelegateCallbacks(version: kCTRunDelegateVersion1, dealloc: { (refCon) -> Void in
    
         }, getAscent: { ( refCon) -> CGFloat in
    
           return config.size.height // 高度
    
         }, getDescent: { (refCon) -> CGFloat in
    
           return 0 // 底部距离
    
         }) { (refCon) -> CGFloat in
    
           return config.size.width // 宽度
    
         }
    
         var imageName = "avatar"
    
         let runDelegate = CTRunDelegateCreate(&imageCallback,&imageName)
    
         // 富文本类型的占位符
    
         let imageAttributedString = NSMutableAttributedString(string: " ")
    
         imageAttributedString.addAttribute(NSAttributedStringKey(rawValue:
    
         kCTRunDelegateAttributeName as String), value: runDelegate!, range:
    
         NSMakeRange(0, 1))     
    
         imageAttributedString.addAttribute(NSAttributedStringKey(rawValue:
    
         "avatarImage"), value: imageName, range: NSMakeRange(0, 1))
    
         // 富文本类型的占位符插到要显示图片的位置
    
         // 这里的设定是图片插在文本行首。。。
         content.insert(imageAttributedString, at: 0)
    
         // 文本绘制同上 多了一个imageUrl信息
        let data: BKCoreTextData = ...
    
         return data
    
    }
    
    

    图片和文本混合怎么显示???

    思路

    • 遍历 CTLine

    • 遍历每个 LineCTRun

    • 通过 CTRunGetAttributes 得到所有属性

    • 通过 KVC 取得属性中的代理属性,图片占位符绑定了代理

    • 判断是否之前设置的图片代理来区分文本和图片

    • 获取图片 距离原点偏移量 来计算图片绘制区域的 CGRect

    • 使用 Core Graphics 异步绘制图片

    
        var data: BKCoreTextData? {
    
         didSet { setNeedsDisplay() }
    
        }
    
        private var avatarImage: Image = #默认占位符
    
        override func draw(_ rect: CGRect) {
    
           super.draw(rect)
    
           guard let context = UIGraphicsGetCurrentContext() else { return }
    
           guard let frame = data?.ctFrame else { return }
    
           context.textMatrix = CGAffineTransform.identity
    
           context.translateBy(x: 0, y: bounds.size.height)
    
           context.scaleBy(x: 1, y: -1)
    
           CTFrameDraw(frame, context)
    
           let lines = CTFrameGetLines(frame) as Array
    
           let count = lines.count
    
           var origins = [CGPoint].init(repeating: CGPoint(0, 0), count: count)
    
           CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins)
    
           for (index, line) in lines.enumerated() {
    
             (CTLineGetGlyphRuns(line as! CTLine) as Array).forEach({
    
               var runAscent : CGFloat = 0
    
               var runDescent : CGFloat = 0
    
               let lineOrigin = origins[index]
    
               let attributes = CTRunGetAttributes($0 as! CTRun)
    
               let width = CGFloat( CTRunGetTypographicBounds($0 as! 
    
               CTRun, CFRangeMake(0,0), &runAscent, &runDescent, nil))
    
               let location = CTLineGetOffsetForStringIndex(line as! CTLine, 
    
               CTRunGetStringRange($0 as! CTRun).location, nil)
    
               let runRect = ccr(lineOrigin.x + location, lineOrigin.y - runDescent, 
    
               width, runAscent + runDescent)
    
               let imageNames = attributes.object(forKey: "avatarImage")
    
               if imageNames is String {
    
                 DispatchQueue.global().async { [weak self] in
    
                 // 获取图片 data.imageUrl
                 let tmp = ....
    
                 DispatchQueue.main.async {
    
                     self?.avatarImage = tmp!
    
                     self?.setNeedsDisplay(runRect)
    
                 }
    
             }
    
             context.draw(avatarImage.cgImage!, in: runRect)
    
           }
    
          })
    
        }
    
    }
    
    

    这是比较理想的图文混合,图片的高度和文本高度差不多,以及图片的位置又刚好在行首。

    组队

    来看个实际需求的图文混合

    0

    绘制的方式是和上面的是一样的,不同在于图片和文本的排版不一样。

    实践步骤:

    0.不做处理

    1

    1.文字都堆在一起了,给文本不同样式划分段落

    2

    2.恩,跟目标很接近了 😀,根据不同段落展示样式,调整行首缩进距离 firstLineHeadIndent 以及基线的距离 baselineOffset

    
    let margin : CGFloat = 20
    
    let paragraphStyle0 = NSMutableParagraphStyle()
    
    paragraphStyle0.alignment = .left
    
    paragraphStyle0.firstLineHeadIndent = image.size.width + margin // 首行缩进
    
    title.addAttributes([.baselineOffset: 15,.paragraphStyle: paragraphStyle0], range: NSMakeRange(0, title.length - 1))
    
    subTitle.addAttributes([.baselineOffset: 10, .paragraphStyle: paragraphStyle0], range: NSMakeRange(0, subTitle.length))
    
    let paragraphStyle1 = NSMutableParagraphStyle()
    
    paragraphStyle1.alignment = .left
    
    paragraphStyle1.firstLineHeadIndent = kScreenWidth - 30 - 20
    
    indicator.addAttributes([.baselineOffset: 28, .paragraphStyle: paragraphStyle1], range: NSMakeRange(0, 1))
    
    

    3.貌似已经达到目的了,这里也可以体现使用 Core Text 的优势,减少不必要的图层。

    3

    但是呢,这只是当前场景一种取巧的方式,hard code 间距,基线距离。如果 title 或者 subTitle 多行,这种方法就失效了。所以类似问题就是要解决图片和文字环绕的排版方式。

    图文环绕

    draw 函数是直接调用 frame 将内容绘制出来的,frame 是怎么来的

    
    let path = CGMutablePath()
    
    path.addRect(CGRect(x: 0, y: 0, width: config.width, height: height))
    
    let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path, nil)
    
    

    frame 是根据指定的 path 生成的,所以如果这个 path 将图片区域去掉,得到的 frame 就不包含该区域。但是这个 frame 里面也不再包含图片信息了。

    
    let path = UIBezierPath(rect: CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height))
    
    // !!! 这个左下角为坐标原点
    let imagePath = UIBezierPath(rect: CGRect(x: 3, y: 3, width: image.size.width, height: image.size.height))
    
    // 减去图片区域
    path.append(imagePath)
    
    let frame = CTFramesetterCreateFrame(frameSetter, CFRangeMake(0, 0), path.cgPath, nil)
    
    
    4-Path

    可以看到整个绘制区域被分成了两个部分,一个是图片一个是文本。通过 UIBezierPath 还可以绘制任何想要的形状。剩下的问题就是处理段落之间的行间距。

    5

    Done!

    总结

    本文只是简单介绍了一些 Core Text 的东西,实际上还是有许多的细节还需要细细磨。实际开发过程中可能业务的形式不一,但是知识点是相通的,灵活应用都能达到目的。希望本文能给使用 CoreText 的同学一些启发。

    参考

    http://blog.devtang.com/2015/06/27/using-coretext-1/
    http://blog.devtang.com/2015/06/27/using-coretext-2/
    https://www.raywenderlich.com/153591/core-text-tutorial-ios-making-magazine-app
    http://blog.cnbang.net/tech/2729/

    相关文章

      网友评论

      本文标题:Swift: 基于 CoreText 图文排版实践

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