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

如何实现多行UILabel最后一行末尾淡出 适配UITableViewCell场景

UILabel 任意行数仅最后一行末尾淡出效果实现

现存方案问题

需要实现兼容UILabel 1/2/3等任意行数场景的文本末尾淡出效果,现有两类方案都存在缺陷:

  • 单行场景下常用的直接给UILabel图层添加CAGradientLayer遮罩的方案,无法适配多行场景:遮罩会覆盖整个图层,导致所有行的文本末尾都出现淡出效果,不符合预期。
  • 自行添加带渐变遮罩的子图层、估算定位到最后一行位置的方案,静态场景下可正常生效,但放在UITableViewCell中使用时,cell点击高亮变灰的场景下,自定义添加的图层会直接显露。问题根源有两点:一是自定义子图层硬编码了白色背景,系统给cell设置高亮色时不会修改这个子图层的颜色;二是用「总高度/行数」的方式估算最后一行位置,没有考虑行间距、自定义字体行高的差异,位置计算存在偏差。

原有存在缺陷的代码如下:

extension UILabel {
    func fadeOutLastLineEnd() { //Call in layoutSubviews
        guard bounds.width > 0 else { return }

        lineBreakMode = .byCharWrapping
        let tmpLayer = CALayer()
        let gradientWidth: CGFloat = 32
        let numberOfLines = CGFloat(numberOfLines)
        tmpLayer.backgroundColor = UIColor.white.cgColor
        tmpLayer.frame = CGRect(x: layer.frame.width - gradientWidth,
                                y: layer.frame.height / numberOfLines,
                                width: gradientWidth,
                                height: layer.frame.height / numberOfLines)
        
        let tmpGrLayer = CAGradientLayer()

        tmpGrLayer.colors     = [UIColor.white.cgColor, UIColor.clear.cgColor]
        tmpGrLayer.startPoint = CGPoint(x: 1, y: 0)
        tmpGrLayer.endPoint   = CGPoint(x: 0, y: 0)
        tmpGrLayer.frame = tmpLayer.bounds
        
        tmpLayer.mask = tmpGrLayer
        layer.addSublayer(tmpLayer)
    }
}

目标效果如下图:
UILabel末尾淡出目标效果

需要满足的核心要求:

  • 支持任意行数的多行文本显示
  • 仅最后一行的末尾实现淡出渐变效果
  • 适配UITableViewCell使用场景,cell点击高亮、选中整体变色时不会出现图层显露异常

正确实现方案

核心思路是放弃添加额外带背景色的子图层,通过TextKit框架精准计算最后一行文本的实际渲染位置,直接给UILabel的layer设置mask遮罩:遮罩除最后一行末尾的渐变区域外全部为不透明状态,既不会影响其他行的显示,也不会因为额外子图层干扰系统的高亮色渲染。

完整实现代码如下:

import UIKit

extension UILabel {
    private struct AssociatedKeys {
        static var fadeMaskKey = "fadeMaskKey"
    }
    
    /// 仅对最后一行末尾应用淡出渐变效果
    /// - Parameter gradientWidth: 渐变区域宽度,默认32pt
    func applyLastLineFade(gradientWidth: CGFloat = 32) {
        // 移除旧遮罩避免重复叠加
        if let oldMask = objc_getAssociatedObject(self, &AssociatedKeys.fadeMaskKey) as? CALayer {
            oldMask.removeFromSuperlayer()
            layer.mask = nil
        }
        
        guard bounds.width > 0, let font = font else { return }
        let contentAttrText = attributedText ?? NSAttributedString(
            string: text ?? "",
            attributes: [.font: font]
        )
        guard !contentAttrText.string.isEmpty else { return }
        
        // 通过TextKit计算最后一行文本的实际渲染frame
        let textStorage = NSTextStorage(attributedString: contentAttrText)
        let layoutManager = NSLayoutManager()
        let textContainer = NSTextContainer(size: CGSize(
            width: bounds.width,
            height: .greatestFiniteMagnitude
        ))
        textContainer.lineBreakMode = lineBreakMode
        textContainer.maximumNumberOfLines = numberOfLines
        textContainer.lineFragmentPadding = 0
        
        layoutManager.addTextContainer(textContainer)
        textStorage.addLayoutManager(layoutManager)
        
        let glyphRange = layoutManager.glyphRange(for: textContainer)
        var lastLineRect: CGRect = .zero
        layoutManager.enumerateLineFragments(forGlyphRange: glyphRange) { _, usedRect, _, _, _ in
            lastLineRect = usedRect
        }
        
        guard !lastLineRect.isEmpty else { return }
        
        // 构建遮罩层:整体不透明,仅最后一行末尾做alpha渐变
        let maskLayer = CALayer()
        maskLayer.frame = bounds
        maskLayer.backgroundColor = UIColor.white.cgColor // 白色对应mask不透明
        
        let gradientMask = CAGradientLayer()
        // 渐变仅修改alpha值,不改变颜色
        gradientMask.colors = [
            UIColor.white.withAlphaComponent(1).cgColor,
            UIColor.white.withAlphaComponent(0).cgColor
        ]
        gradientMask.startPoint = CGPoint(x: 0, y: 0.5)
        gradientMask.endPoint = CGPoint(x: 1, y: 0.5)
        // 渐变区域对齐最后一行末尾,适配文本未占满整行的场景
        let gradientStartX = max(lastLineRect.maxX - gradientWidth, lastLineRect.minX)
        gradientMask.frame = CGRect(
            x: gradientStartX,
            y: lastLineRect.minY,
            width: min(gradientWidth, lastLineRect.width),
            height: lastLineRect.height
        )
        
        maskLayer.addSublayer(gradientMask)
        layer.mask = maskLayer
        
        // 关联保存遮罩层,方便下次更新时清理
        objc_setAssociatedObject(
            self,
            &AssociatedKeys.fadeMaskKey,
            maskLayer,
            .OBJC_ASSOCIATION_RETAIN_NONATOMIC
        )
    }
}

使用注意事项

  • 方法需要在布局完成后调用,推荐重写UILabel的layoutSubviews方法,在super.layoutSubviews()之后调用applyLastLineFade()即可自动适配布局变化。
  • 在UITableViewCell中使用时,在cell的layoutSubviews方法中调用对应label的该方法即可,系统处理cell高亮、选中状态时不会出现图层显露问题。
  • 自动布局、手动布局、自定义行高、带行间距的富文本场景都可兼容,TextKit会返回和系统渲染完全一致的最后一行位置,不会出现偏移。

内容的提问来源于stack exchange,提问作者N. Rysin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 07:42:18