XZ_Swift之TextKit类实现把文字变成别的颜色且可交互

TextKit类结构介绍
TextKit 是 iOS7推出的,面向对象的文本处理框架;

借用一张图来介绍一下:

图中真正的引用关系是实线的引用,反方向的虚线是弱引用
具体解读
1>NSTextStorage 负责存储属性文本;
2>NSLayoutManager 用来布局的,负责绘制 3个方法: 绘制字形、绘制背景(比如有些要设置底色的)、布局字形(知道用户点到哪里,点到第几个字符了);
3>NSTextContainer 一般不怎么用,它只是用来指定区域,在 layoutSubviews 设置关联就可以,就是来设定文本显示区域的
TextKit作用:
把某段文字变成别的颜色,把一段文本高亮,且这段文字还可以跟用户交互, 这也是 图文混排 里面非常重要的一个技能
实现效果:点击到网址部分,文字高亮,跳转到下一个页面

实现思路:
1.使用 TextKit 接管 Label 的底层实现 - ‘绘制’ textStorage 的文本内容
2.使用正则表达式过滤 URL
3.交互
代码实现:
class XZLabel: UILabel {
    
    // MARK: - 重写属性 - 进一步体会 TextKit 接管底层的实现
    // 一旦内容变化,需要让 textStorage 响应变化!
    override var text: String? {
        didSet { // 重写准备文本内容
            prepareTextContent()
        }
    }
    
    override var attributedText: NSAttributedString? {
        didSet { // 重写准备文本内容
            prepareTextContent()
        }
    }
    
    override var font: UIFont! {
        didSet { // 重写准备文本内容
            prepareTextContent()
        }
    }
    
    override var textColor: UIColor! {
        didSet { // 重写准备文本内容
            prepareTextContent()
        }
    }
    
    // MARK: - 构造函数
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        prepareTextSystem()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        prepareTextSystem()
    }
    
    // MARK: - 和 URL 的文本交互
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        // 1.获取用户点击的位置
        guard let location = touches.first?.location(in: self) else {
            return
        }
        
        // 2.获取当前点中字符的索引
        let idx = layoutManager.glyphIndex(for: location, in: textContainer)
        
        print("点我了 \(idx)")
        
        // 3.判断 idx 是否在 urls 的 ranges 范围内,如果在,就高亮
        for rang in urlRanges ?? []{
            
            // 返回 bool 值
            if NSLocationInRange(idx, rang) {
                // 如果在范围内,需要高亮
                // 修改文本的字体属性
            textStorage.addAttributes([NSAttributedStringKey.foregroundColor : linkTextColor], range: rang)
                
                // 需要调用 setNeedsDisplay 函数重绘,但是不是 drawRect
                setNeedsDisplay()
            }else {
                print("没戳中")
            }
        }
    }
    
    /// 绘制文本
    /**
     - 在 iOS 中绘制工作是类似于‘油画’似的,后绘制的内容,会把之前绘制的内容覆盖!
     - 尽量避免使用带透明度的颜色,会严重影响性能!
     - UILabel 默认不能实现垂直顶部对齐,使用 TextKit 的当前方法可以实现
     */
    override func drawText(in rect: CGRect) {
        
        let range = NSRange(location: 0, length: textStorage.length)
        
        // 绘制背景 - 注意:绘制背景要放在绘制'字形'前面,否则会覆盖 '字形'
        layoutManager.drawBackground(forGlyphRange: range, at: CGPoint())
        
        // 绘制 Glyphs 字形
        layoutManager.drawGlyphs(forGlyphRange: range, at: CGPoint())
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // 指定文本绘制的区域
        textContainer.size = bounds.size
    }
    
    /// 链接颜色
    public var linkTextColor = UIColor.blue
    /// 选中背景色
    public var selectedBackgroundColor = UIColor.init(white: 0.8, alpha: 1.0)
    
    // MARK: - TextKit 的核心对象
    /// 属性文本存储
    private lazy var textStorage = NSTextStorage()
    /// 负责文本'字形'布局
    private lazy var layoutManager = NSLayoutManager()
    /// 设置文本绘制的范围
    private lazy var textContainer = NSTextContainer()
}

// MARK: - 正则表达式函数
private extension XZLabel {
    
    /// 返回 textStorage 中的 URL range 数组
    var urlRanges : [NSRange]? {
        // 1. url 的正则表达式
        let pattern = "[a-zA-Z]*://[a-zA-Z0-9/\\.]*"
        
        guard let regx = try? NSRegularExpression(pattern: pattern, options: []) else {
            return nil
        }
        
        // 2.多重匹配
        let matches = regx.matches(in: textStorage.string, options: [], range: NSRange(location: 0, length: textStorage.length))
        
        // 3.遍历数组,生成 range 的数组
        var ranges = [NSRange]()
        
        for m in matches {
            ranges.append(m.range(at: 0))
        }
        
        return ranges
    }
}

// MARK: - 设置 TextKit 核心对象
private extension XZLabel {
    
    /// 准备文本系统
    func prepareTextSystem() {
        // 0.开启用户交互
        isUserInteractionEnabled = true
        
        // 1. 准备文本内容
        prepareTextContent()
        
        // 2.设置对象的关系
        textStorage.addLayoutManager(layoutManager)
        layoutManager.addTextContainer(textContainer)
    }
    
    /// 准备文本内容 - 使用 textStorage 接管 label 的内容
    func prepareTextContent() {
        
        if let attributedText = attributedText {
            textStorage.setAttributedString(attributedText)
        }else if let text = text {
            textStorage.setAttributedString(NSAttributedString(string: text))
        }else {
            textStorage.setAttributedString(NSAttributedString(string: ""))
        }
        
        // 范围是 - Optional([{3, 20}])
        print("范围是 - \(urlRanges)")
        
        // 遍历范围数组,设置 URL 文字属性,URL 特殊显示
        for rang in urlRanges ?? [] {
            textStorage.addAttributes(
                [
                    NSAttributedStringKey.foregroundColor : UIColor.red,
                    NSAttributedStringKey.backgroundColor : selectedBackgroundColor
                ],
                range: rang)
        }
    }



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值