如何用UILabel实现类似Instagram的‘...更多’文本截断功能
iOS UILabel 两行显示末尾追加"...More"的高效实现方案
你的核心需求是让UILabel仅显示两行文本,并在末尾追加自定义的"...More"提示,但当前实现的循环计算逻辑重复调用高成本的文本布局接口,导致性能低下、屏幕闪烁。以下是针对问题的优化方案:
原代码的性能问题点
- 多次重复调用
boundingRect(with:options:attributes:context:):该方法涉及文本排版计算,本身开销较大,循环内反复调用会显著拖慢性能 - 字符拆分+循环删除的低效逻辑:每次修改
text属性都会触发UILabel的布局刷新,频繁操作直接导致屏幕闪烁 - 冗余的行数计算方法:三个
getNumberOfLines方法逻辑重复,没有复用计算结果
方案一:利用系统文本引擎实现高效截断(推荐)
直接借助iOS原生的文本布局能力,只做一次计算即可完成需求,性能最优且避免布局抖动:
import UIKit extension UILabel { func setupTwoLineWithMoreIndicator(moreText: String = "...More", moreTextColor: UIColor = .gray) { self.numberOfLines = 2 self.lineBreakMode = .byTruncatingTail guard let originalText = self.text, !originalText.isEmpty else { return } // 定义"...More"的样式 let moreAttrs: [NSAttributedString.Key: Any] = [ .font: self.font, .foregroundColor: moreTextColor ] let moreAttributedStr = NSAttributedString(string: moreText, attributes: moreAttrs) // 计算两行文本的最大尺寸 let maxLineHeight = self.font.lineHeight * CGFloat(self.numberOfLines) let maxSize = CGSize(width: self.bounds.width, height: maxLineHeight) let originalAttrStr = NSAttributedString(string: originalText, attributes: [.font: self.font]) // 获取系统截断后的有效文本范围 let truncationRange = originalAttrStr.getTruncatedRange(for: maxSize) if truncationRange.upperBound < originalAttrStr.length { // 拼接截断后的文本和自定义提示 let truncatedStr = originalAttrStr.attributedSubstring(from: NSRange(location: 0, length: truncationRange.upperBound)) let finalAttrStr = NSMutableAttributedString(attributedString: truncatedStr) finalAttrStr.append(moreAttributedStr) self.attributedText = finalAttrStr } } } // 扩展NSAttributedString获取截断范围 private extension NSAttributedString { func getTruncatedRange(for maxSize: CGSize) -> NSRange { let textStorage = NSTextStorage(attributedString: self) let layoutManager = NSLayoutManager() textStorage.addLayoutManager(layoutManager) let textContainer = NSTextContainer(size: maxSize) textContainer.lineBreakMode = .byTruncatingTail textContainer.maximumNumberOfLines = Int(maxSize.height / (self.attributes(at: 0, effectiveRange: nil)[.font] as? UIFont ?? .systemFont(ofSize: 17)).lineHeight) layoutManager.addTextContainer(textContainer) var range = NSRange(location: 0, length: self.length) layoutManager.glyphRange(forBoundingRect: CGRect(origin: .zero, size: maxSize), in: textContainer, actualGlyphRange: &range) // 调整范围,避免截断到字符中间 if range.length < self.length { range = self.string.rangeOfComposedCharacterSequence(at: range.location + range.length - 1).nsRange } return range } }
方案优势
- 仅调用一次文本布局计算,性能远高于手动循环
- 一次性设置
attributedText,避免多次修改触发的布局刷新,彻底解决闪烁问题 - 支持自定义"...More"的字体、颜色等样式
- 基于系统引擎,截断逻辑更准确,兼容复杂字符(如emoji、多语言)
方案二:优化原逻辑,用二分查找替代循环删除
如果坚持使用类似原代码的思路,可通过二分查找减少布局计算次数,优化性能:
extension UILabel { func replaceEllipsisOptimized(with moreText: String, maxLine: Int = 2, highlightColor: UIColor = .gray) { guard let originalText = self.text, !originalText.isEmpty else { return } let font = self.font let width = self.bounds.width let lineHeight = font.lineHeight let maxAllowedHeight = lineHeight * CGFloat(maxLine) // 判断是否需要截断 let fullTextHeight = originalText.boundingRect( with: CGSize(width: width, height: .greatestFiniteMagnitude), options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil ).height if fullTextHeight <= maxAllowedHeight { return } // 二分查找最大可显示的文本长度 var left = 0 var right = originalText.count var bestLength = 0 while left <= right { let mid = (left + right) / 2 let testString = String(originalText.prefix(mid)) + moreText let testHeight = testString.boundingRect( with: CGSize(width: width, height: .greatestFiniteMagnitude), options: .usesLineFragmentOrigin, attributes: [.font: font], context: nil ).height if testHeight <= maxAllowedHeight { bestLength = mid left = mid + 1 } else { right = mid - 1 } } // 生成最终富文本 let finalText = String(originalText.prefix(bestLength)) + moreText let attributedString = NSMutableAttributedString(string: finalText) let moreRange = NSRange( location: finalText.count - moreText.count, length: moreText.count ) attributedString.addAttribute(.foregroundColor, value: highlightColor, range: moreRange) self.attributedText = attributedString self.numberOfLines = maxLine } }
优化点说明
- 用二分查找替代循环删除,时间复杂度从O(n)降至O(logn)
- 减少
boundingRect调用次数,仅在二分查找的每一步计算一次 - 一次性设置
attributedText,避免多次布局刷新
内容的提问来源于stack exchange,提问作者red
相关产品推荐
相关产品推荐

