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

使用CircularCustomTransition后自定义转场动画出现残留视图求助

问题原因分析与修复方案

核心问题根源

你遇到的黄色残留视图问题,本质是转场上下文管理混乱和视图/图层未正确清理导致的,具体来看:

1. 多次调用completeTransition(_:)破坏转场流程

在CircularTransition中,你在四个不同位置都调用了context?.completeTransition(_:):

  • animateOldTextOffscreen的completion闭包
  • animate方法内直接调用
  • animateToTextView的completion闭包
  • CAAnimationDelegate的animationDidStop(_:finished:)方法

completeTransition(_:)是UIKit转场的终结方法,只能被调用一次。多次调用会让UIKit的转场状态异常,容器视图中的临时视图(快照、背景视图)无法被自动清理,这些残留视图就会在后续转场中显示出来。

2. 手动操作视图层级违反转场规范

你在animateTransition中直接执行了fromVC.mainView.removeFromSuperview(),跳过了UIKit的转场视图管理逻辑。转场过程中,视图的添加/移除应该完全由transitionContext处理,手动操作会导致视图引用残留。

3. 临时视图与图层未主动清理

  • 转场中添加的snapshot和backgroundView没有在转场完成后移除,会堆积在容器视图中
  • 给toVC.mainView.layer添加的maskLayer在动画结束后未被移除,若转场流程异常,这个mask会残留到其他视图上

4. 其他动画控制器的潜在问题

CustomBounceUpAnimationController中重复写了两段完全相同的UIView.animate代码,每次都会调用completeTransition(true),进一步加重了转场上下文的混乱。


分步修复方案

一、修复CircularTransition核心问题

1. 确保completeTransition(_:)只调用一次

用UIView.animateKeyframes统一管理所有UI动画,在所有动画完成后仅调用一次转场终结方法:

override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    guard let fromVC = transitionContext.viewController(forKey: .from) as? CircleTransitionable,
          let toVC = transitionContext.viewController(forKey: .to) as? CircleTransitionable,
          let snapshot = fromVC.mainView.snapshotView(afterScreenUpdates: false) else {
        transitionContext.completeTransition(false)
        return
    }
    
    context = transitionContext
    let containerView = transitionContext.containerView
    
    // 添加临时背景视图
    let backgroundView = UIView()
    backgroundView.frame = toVC.mainView.frame
    backgroundView.backgroundColor = fromVC.mainView.backgroundColor
    containerView.addSubview(backgroundView)
    
    // 添加转场所需视图(不要手动移除fromVC的view)
    containerView.addSubview(snapshot)
    containerView.addSubview(toVC.mainView)
    
    // 初始化文本视图的初始状态
    let originalCenter = toVC.contentTextView.center
    toVC.contentTextView.alpha = 0.0
    toVC.contentTextView.center = fromVC.triggerButton.center
    toVC.contentTextView.transform = CGAffineTransform(scaleX: 0.1, y: 0.1)
    
    // 用关键帧动画统一管理UI动画序列
    UIView.animateKeyframes(withDuration: 0.35, delay: 0, options: [], animations: {
        // 旧视图滑出动画
        UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.25) {
            snapshot.center = CGPoint(x: snapshot.center.x - 1000, y: snapshot.center.y + 1500)
            snapshot.transform = CGAffineTransform(scaleX: 5.0, y: 5.0)
        }
        
        // 文本淡入动画(延迟0.1s开始)
        UIView.addKeyframe(withRelativeStartTime: 0.1/0.35, relativeDuration: 0.25/0.35) {
            toVC.contentTextView.transform = .identity
            toVC.contentTextView.center = originalCenter
            toVC.contentTextView.alpha = 1.0
        }
    }, completion: { [weak self] finished in
        // 清理临时视图
        snapshot.removeFromSuperview()
        backgroundView.removeFromSuperview()
        // 标记转场完成,同时处理取消场景
        transitionContext.completeTransition(finished && !transitionContext.transitionWasCancelled)
        self?.context = nil
    })
    
    // 单独处理圆形mask的CA动画
    animateCircleMask(toView: toVC.mainView, fromTriggerButton: fromVC.triggerButton)
}

2. 重构圆形mask动画并清理图层

修改animate方法为animateCircleMask,并在动画结束后移除mask:

func animateCircleMask(toView: UIView, fromTriggerButton triggerButton: UIButton) {
    // Starting Path
    let rect = CGRect(x: triggerButton.frame.origin.x, y: triggerButton.frame.origin.y, width: triggerButton.frame.width, height: triggerButton.frame.width)
    let circleMaskPathInitial = UIBezierPath(ovalIn: rect)
    
    // Destination Path
    let fullHeight = toView.bounds.height
    let extremePoint = CGPoint(x: triggerButton.center.x, y: fullHeight)
    let radius = sqrt((extremePoint.x*extremePoint.x) + (extremePoint.y*extremePoint.y))
    let circleMaskPathFinal = UIBezierPath(ovalIn: triggerButton.frame.insetBy(dx: -radius, dy: -radius))
    
    // Actual mask layer
    let maskLayer = CAShapeLayer()
    maskLayer.path = circleMaskPathFinal.cgPath
    toView.layer.mask = maskLayer
    
    // Mask Animation
    let maskLayerAnimation = CABasicAnimation(keyPath: "path")
    maskLayerAnimation.fromValue = circleMaskPathInitial.cgPath
    maskLayerAnimation.toValue = circleMaskPathFinal.cgPath
    maskLayerAnimation.duration = 0.25
    maskLayerAnimation.delegate = self
    maskLayer.add(maskLayerAnimation, forKey: "path")
}

更新CAAnimationDelegate方法,仅清理mask不再调用completeTransition:

extension CircularTransition: CAAnimationDelegate {
    func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
        // 动画结束后移除mask,避免图层残留
        context?.viewController(forKey: .to)?.view.layer.mask = nil
    }
}

3. 移除手动操作fromVC视图的代码

删除fromVC.mainView.removeFromSuperview(),转场完成后UIKit会自动处理fromVC视图的移除。


二、修复CustomBounceUpAnimationController的重复动画问题

删除重复的UIView.animate代码,确保只调用一次转场终结方法:

class CustomBounceUpAnimationController: CustomAnimator {
    override func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 2.5
    }
    
    override func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let fromViewController = transitionContext.viewController(forKey: .from)!
        let toViewController = transitionContext.viewController(forKey: .to)!
        let finalFrameForVC = transitionContext.finalFrame(for: toViewController)
        let containerView = transitionContext.containerView
        
        containerView.bringSubviewToFront(toViewController.view)
        let bounds = UIScreen.main.bounds
        toViewController.view.frame = finalFrameForVC.offsetBy(dx: 0.0, dy: bounds.size.height)
        containerView.addSubview(toViewController.view)
        
        UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.0, options: .curveLinear, animations: {
            fromViewController.view.alpha = 0.5
            toViewController.view.frame = finalFrameForVC
        }) { finished in
            fromViewController.view.alpha = 1.0
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
        }
    }
}

额外最佳实践建议

  • 始终依赖transitionContext提供的视图和框架,不要直接操作VC的视图层级
  • 对于CAAnimation,务必在动画结束后清理相关图层(比如mask)
  • 转场完成后及时将context置为nil,避免循环引用(你已经用了weak var context,保持这个好习惯)
  • 测试转场取消场景,确保在transitionWasCancelled为true时正确恢复视图状态

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 08:10:07