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 } }
关键修改点说明
- 禁用布局动画:用
UIView.performWithoutAnimation包裹文本更新操作,消除系统默认的布局动画导致的闪烁。 - 动画状态控制:增加
isAnimating标记,防止重复触发动画导致层叠加。 - 层移除时机优化:动画完成后直接移除动画层,不再依赖
layoutSublayers的时机,减少布局冲突。 - 样式一致性:动画层直接复用UITextView的字体和文本颜色,避免样式差异。
- 重复调用防护:在Coordinator中跟踪已处理的文本长度,防止
updateUIView重复触发动画。
内容的提问来源于stack exchange,提问作者Cheng Ma
相关产品推荐
相关产品推荐

