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

Swift实现带渐入动画的UITextView遇文本闪烁问题求助

解决UITextView文本淡入动画后闪烁的问题

核心问题分析

你遇到的闪烁是因为更新UITextView.text时会触发系统布局刷新,默认的UIKit布局动画会导致文本重绘时出现短暂闪烁;同时原代码中层移除的时机与文本更新的时机不匹配,加剧了这个问题。

修改后的完整代码

自定义AnimatedTextView

class AnimatedTextView: UITextView {

    private var animatingTextLayer: CATextLayer?
    private var isAnimating = false

    func appendTextAndAnimate(with newText: String) {
        guard let font = self.font else { return }
        guard !isAnimating else { return } // 防止重复触发动画

        isAnimating = true
        let textLayerFrame = calculateTextLayerFrame(for: newText, in: self)

        // 创建动画层,确保样式与原生文本一致
        let textLayer = CATextLayer()
        textLayer.string = newText
        textLayer.foregroundColor = self.textColor?.cgColor ?? UIColor.black.cgColor
        textLayer.font = font
        textLayer.fontSize = font.pointSize
        textLayer.alignmentMode = .left
        textLayer.frame = textLayerFrame
        textLayer.opacity = 0
        self.layer.addSublayer(textLayer)
        self.animatingTextLayer = textLayer

        // 淡入动画配置
        let fadeAnimation = CABasicAnimation(keyPath: "opacity")
        fadeAnimation.fromValue = 0
        fadeAnimation.toValue = 1
        fadeAnimation.duration = 0.5
        fadeAnimation.fillMode = .forwards
        fadeAnimation.isRemovedOnCompletion = false

        CATransaction.begin()
        CATransaction.setCompletionBlock { [weak self] in
            guard let self = self else { return }

            // 禁用UIKit动画,避免文本更新时的布局闪烁
            UIView.performWithoutAnimation {
                self.text.append(newText)
            }

            // 动画完成后直接移除层,避免布局冲突
            textLayer.removeFromSuperlayer()
            self.animatingTextLayer = nil
            self.isAnimating = false
        }
        textLayer.add(fadeAnimation, forKey: "fadeIn")
        CATransaction.commit()
    }

    private func calculateTextLayerFrame(for newText: String, in textView: UITextView) -> CGRect {
        guard let font = textView.font else { return .zero }

        let currentTextLength = textView.text.count
        let layoutManager = textView.layoutManager
        let textContainer = textView.textContainer

        // 获取当前文本末尾的位置
        let characterRange = NSRange(location: currentTextLength, length: 0)
        let glyphRange = layoutManager.glyphRange(forCharacterRange: characterRange, actualCharacterRange: nil)
        let glyphRect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)

        // 计算偏移后的位置(适配textContainerInset)
        let x = glyphRect.origin.x + textView.textContainerInset.left
        let y = glyphRect.origin.y + textView.textContainerInset.top

        // 计算新文本的尺寸
        let textSize = (newText as NSString).size(withAttributes: [.font: font])

        return CGRect(x: x, y: y, width: textSize.width, height: textSize.height)
    }

    override func layoutSublayers(of layer: CALayer) {
        super.layoutSublayers(of: layer)
        // 兜底逻辑:如果动画层存在但未在动画中,强制移除
        if let animatingLayer = animatingTextLayer, !isAnimating {
            animatingLayer.removeFromSuperlayer()
            animatingTextLayer = nil
        }
    }
}

UIViewRepresentable封装

struct TextViewRepresentable: UIViewRepresentable {
    var content: String

    class Coordinator: NSObject {
        var processedLength = 0 // 跟踪已处理的文本长度,避免重复触发动画
    }

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }

    func makeUIView(context: Context) -> AnimatedTextView {
        let textView = AnimatedTextView()
        textView.isEditable = false
        textView.isScrollEnabled = true
        return textView
    }

    func updateUIView(_ uiView: AnimatedTextView, context: Context) {
        let currentProcessedLength = context.coordinator.processedLength
        guard content.count > currentProcessedLength else { return }

        let newText = String(content.dropFirst(currentProcessedLength))
        uiView.appendTextAndAnimate(with: newText)
        context.coordinator.processedLength = content.count
    }
}

关键修改点说明

  1. 禁用布局动画:用UIView.performWithoutAnimation包裹文本更新操作,消除系统默认的布局动画导致的闪烁。
  2. 动画状态控制:增加isAnimating标记,防止重复触发动画导致层叠加。
  3. 层移除时机优化:动画完成后直接移除动画层,不再依赖layoutSublayers的时机,减少布局冲突。
  4. 样式一致性:动画层直接复用UITextView的字体和文本颜色,避免样式差异。
  5. 重复调用防护:在Coordinator中跟踪已处理的文本长度,防止updateUIView重复触发动画。

内容的提问来源于stack exchange,提问作者Cheng Ma

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 06:00:05