You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 09:09:53