Swift-UILabel实现用户协议图文混排

3,924 阅读3分钟

前言

最近换了一个工作,有点忙,记录笔记的时间越来越少了。关于RxSwift专栏的文章,笔者后续再抽时间记录。今天主要分享一个笔者碰到的一个需求,在实现的过程中决定封装一下,希望可以帮到有需要的小伙伴。\

需求:实现红色框框内的效果

image.png

要求:

  • 按钮支持点击选中和反选
  • 协议支持点击
  • 支持换行
  • 截断时也要支持点击
  • 图文中心保持一致

分析:

前3点不难,用按钮label组合一下即可,后面两点就要求必须使用富文本了,因为截断的情况不固定,比如:

image.png

或者

image.png

这两种都不好处理,用按钮无法实现截断的处理。

实现

处理图片

图片的比较容易处理一些,将图片转成富文本,和后面内容拼接即可,这里需要注意的是图片的中心点要和后面的文案对齐,对齐可以采用基线偏移来处理(偏移的是后面的文案)

            let baselineOffset = (largeFont.lineHeight - normalFont.lineHeight)/2.0 + (largeFont.descender - normalFont.descender)
            muAttributedString.addAttributes([NSAttributedString.Key.font: normalFont,
                                              NSAttributedString.Key.baselineOffset: baselineOffset],
                                             range: NSRange(location: normalImgAttrString.length, length: muAttributedString.length - normalImgAttrString.length))

获取文案的区域

通常我们想的是直接使用这个方法来获取

layoutManager.boundingRect(forGlyphRange: , in: )

这个方法对于第一种情况是可以解决的,但是后面截断的情况无法处理。

翻看layoutManager的API发现有一个方法可以处理截断的情况

open func enumerateEnclosingRects(forGlyphRange glyphRange: NSRange, withinSelectedGlyphRange selectedRange: NSRange, in textContainer: NSTextContainer, using block: @escaping (CGRect, UnsafeMutablePointer<ObjCBool>) -> Void)

枚举textContainer中glyphRange的封闭矩形。如果在第二个参数中给出了所选范围,则返回的矩形将正确用于绘制所选内容。

从官方的注解可以发现,将给定范围的文字的所有区域枚举一遍。从而通过第一个矩形和最后一个矩形即可覆盖所有的文字范围,如下图:

image.png

实现代码:

func rectFor(string str : String, fromIndex: Int = 0) -> (CGRect, CGRect)?
    {
        // Find the range of the string
        guard self.text != nil else { return nil }
        let subStringToSearch : NSString = (self.text! as NSString).substring(from: fromIndex) as NSString
        var stringRange = subStringToSearch.range(of: str)
        if (stringRange.location != NSNotFound)
        {
            guard self.attributedText != nil else { return nil }
            // Add the starting point to the sub string
            stringRange.location += fromIndex
            let storage = NSTextStorage(attributedString: self.attributedText!)
            let layoutManager = NSLayoutManager()
            storage.addLayoutManager(layoutManager)
            let textContainer = NSTextContainer(size: self.frame.size)
            textContainer.lineFragmentPadding = 0
            textContainer.lineBreakMode = .byWordWrapping
            layoutManager.addTextContainer(textContainer)
            var glyphRange = NSRange()
            
            layoutManager.characterRange(forGlyphRange: stringRange, actualGlyphRange: &glyphRange)
            var firstWordRect = true
            var rect1 = CGRectZero
            var rect2 = CGRectZero
            layoutManager.enumerateEnclosingRects(forGlyphRange: glyphRange, withinSelectedGlyphRange: NSRange(location: NSNotFound, length: 1), in: textContainer) { wordRect, isStop in
                if firstWordRect {
                    rect1 = wordRect
                    firstWordRect = false
                }
                
                rect2 = wordRect
            }
            
            return (rect1, rect2)
        }
        return nil
    }

点击处理

@objc
    private func click(_ gesture: UITapGestureRecognizer) {
        
        guard let dict = callBackMap else {
            return
        }
        
        let point = gesture.location(in: self)
        for key in dict.keys {
            let (rect1, rect2) = rectFor(string: key)!
            let imgString = selected ? selectedImgAttrString : normalImgAttrString
            
            if containsPoint(minRect: rect1, maxRect: key == imgString.string ? CGRect(x: 0, y: 0, width: largeFont.pointSize, height: largeFont.pointSize):rect2, point: point) {
                
                if key == imgString.string {
                    selected = !selected
                }
                if let callBack = dict[key] {
                    callBack()
                }
            }
        }
        
    }

总结

笔者封装了一个UILabel的扩展,可方便使用。完整的demo已放在github,有兴趣的小伙伴可以去下载用一下看看

注意事项:

图片和largefont的大小最好保持一致,不然基线偏移的时候要特殊处理,这种情况在代码里做了注解

image.png


更新

问题:

如果有多个重复的协议,第一个可以点击,后面的重复协议不支持点击

解决:

1.文本颜色:添加协议时,遍历所有文本,得到该字串的所有range,从而修改文本颜色。
2.点击处理:同样遍历字串的所有range,直到点击的位置落在range区域内,或者查询无果不做响应

阶段性总结

笔者在遇到问题时,会尽量的解决,也欢迎小伙伴把碰到的问题提出来,一起学习进步。
在此感谢小伙伴season_zhu的提示,demo链接已更新。
支持pod使用:
pod 'UILabelImageText'