iOS中UILabel设置textAlignment为.justified时,layoutManager.boundingRect返回错误矩形的问题
iOS中UILabel设置textAlignment为.justified时,layoutManager.boundingRect返回错误矩形的问题
兄弟,我前阵子做类似的富文本链接高亮功能时,刚好踩过这个两端对齐的坑!当时我也是自定义UILabel子类,用NSLayoutManager、NSTextStorage和NSTextContainer这套组合来给链接加圆角背景,结果设置textAlignment = .justified之后,layoutManager.boundingRect返回的矩形要么飘到别的位置,要么大小完全不对,折腾了好一阵才摸清楚问题根源。
问题到底出在哪?
其实核心原因是两端对齐的文本排版高度依赖容器的实时尺寸和最新布局状态:
- 如果你初始化
textContainer后没同步UILabel的lineFragmentPadding、lineBreakMode这些排版属性,layoutManager的计算规则会和UILabel本身的排版逻辑不一致; - 当UILabel的bounds变化(比如屏幕旋转、父视图布局更新)时,
textContainer的size没有及时更新,layoutManager还是基于旧的尺寸计算矩形; - 两端对齐时每一行的字符间距会动态拉伸,直接用整个字符范围去拿
boundingRect,会忽略行内的排版细节,导致返回的rect和实际显示的文本位置不匹配。
我亲测有效的解决办法
1. 初始化同步属性+监听容器尺寸变化
首先在自定义Label的初始化方法里,把UILabel的关键排版属性同步给textContainer,并且监听bounds变化,实时更新textContainer的size:
import UIKit class MyLabel: UILabel { private var linkRects = [[CGRect]]() private let layoutManager = NSLayoutManager() private let textContainer = NSTextContainer(size: .zero) private let textStorage = NSTextStorage() override init(frame: CGRect) { super.init(frame: frame) setupTextSystem() } required init?(coder: NSCoder) { super.init(coder: coder) setupTextSystem() } private func setupTextSystem() { // 绑定三大组件的关系 layoutManager.addTextContainer(textContainer) textStorage.addLayoutManager(layoutManager) // 同步UILabel的排版规则到textContainer textContainer.lineFragmentPadding = self.lineFragmentPadding textContainer.lineBreakMode = self.lineBreakMode textContainer.maximumNumberOfLines = self.numberOfLines // 监听bounds变化,实时更新容器尺寸 addObserver(self, forKeyPath: #keyPath(bounds), options: [.new], context: nil) } override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { if keyPath == #keyPath(bounds), let newBounds = change?[.newKey] as? CGRect { textContainer.size = newBounds.size updateLinkRects() setNeedsDisplay() } } deinit { // 记得移除观察者,避免内存泄漏 removeObserver(self, forKeyPath: #keyPath(bounds)) } }
2. 逐行计算链接的实际矩形
别直接用整个字符范围拿boundingRect了,把字符范围转成glyph范围,再逐行枚举每个行片段的rect,这样就能精准拿到两端对齐后每个链接的实际位置:
extension MyLabel { private func updateLinkRects() { linkRects.removeAll() guard let attributedText = self.attributedText else { return } textStorage.setAttributedString(attributedText) // 强制layoutManager完成最新布局计算 layoutManager.ensureLayout(for: textContainer) // 遍历所有带link属性的文本片段 attributedText.enumerateAttribute(.link, in: NSRange(location: 0, length: attributedText.length), options: []) { _, range, _ in let glyphRange = self.layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil) var lineRects = [CGRect]() // 逐行处理当前link所在的行 self.layoutManager.enumerateLineFragments(forGlyphRange: glyphRange) { [weak self] _, _, container, lineGlyphRange, _ in guard let self = self else { return } // 取当前行和link的glyph范围的交集 let intersectionRange = NSIntersectionRange(glyphRange, lineGlyphRange) guard intersectionRange.length > 0 else { return } // 获取当前行内link片段的真实rect let glyphRect = self.layoutManager.boundingRect(forGlyphRange: intersectionRange, in: container) // 转换到UILabel自身的坐标系 let convertedRect = self.convert(glyphRect, from: self.layoutManager.textContainerCoordinateSpace(container)) lineRects.append(convertedRect) } self.linkRects.append(lineRects) } } }
3. 在draw方法里绘制链接背景
最后用计算好的linkRects在draw(_:)里绘制背景就行,还可以调整inset来控制背景和文本的间距:
extension MyLabel { override func draw(_ rect: CGRect) { super.draw(rect) // 绘制链接的圆角背景 for lineRects in linkRects { for linkRect in lineRects { let backgroundRect = linkRect.insetBy(dx: -3, dy: 2) let path = UIBezierPath(roundedRect: backgroundRect, cornerRadius: 4) UIColor.systemBlue.withAlphaComponent(0.2).setFill() path.fill() } } } // 当textAlignment变化时,重新计算链接rect override var textAlignment: NSTextAlignment { didSet { updateLinkRects() setNeedsDisplay() } } // 当attributedText变化时,也重新计算 override var attributedText: NSAttributedString? { didSet { updateLinkRects() setNeedsDisplay() } } }
小提醒
- 只要
attributedText、textAlignment或者UILabel的尺寸变了,都要重新计算链接rect并触发重绘; ensureLayout(for:)这步不能省,不然可能拿到旧的布局结果;- 坐标系转换一定要做,layoutManager返回的rect是基于textContainer的,转成UILabel的坐标系才能正确绘制。
内容来源于stack exchange
相关产品推荐
相关产品推荐

