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

Swift实现ViewController的UIImage转场动画(仿N26效果)

实现类似N26的ViewController转场动画 + 拖拽关闭

我完全get你想要的那种和N26一样丝滑的转场效果,结合你现有的CollectionView点击跳转场景,咱们可以通过自定义转场动画+拖拽手势来实现,一步步来:

一、先搞定自定义转场动画

首先创建两个动画类,分别处理present(从Cell展开到全屏)和dismiss(从全屏缩回Cell)的逻辑:

1. Present动画类(从点击Cell位置展开)

import UIKit

class PresentAnimation: NSObject, UIViewControllerAnimatedTransitioning {
    // 记录点击Cell在屏幕上的初始位置
    private let originFrame: CGRect
    
    init(originFrame: CGRect) {
        self.originFrame = originFrame
        super.init()
    }
    
    // 动画时长,可根据N26的效果微调
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.4
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let targetVC = transitionContext.viewController(forKey: .to) as? WishlistViewController,
              let container = transitionContext.containerView else { return }
        
        // 先把目标VC的View缩放到Cell的大小和位置,保持和Cell一致的圆角
        targetVC.view.frame = originFrame
        targetVC.view.layer.cornerRadius = 16 // 假设你的Cell圆角是16,按需调整
        container.addSubview(targetVC.view)
        
        // 用弹簧动画实现自然的展开效果
        UIView.animate(withDuration: transitionDuration(using: transitionContext),
                       delay: 0,
                       usingSpringWithDamping: 0.8,
                       initialSpringVelocity: 0.6,
                       options: .curveEaseOut) {
            targetVC.view.frame = transitionContext.finalFrame(for: targetVC)
            targetVC.view.layer.cornerRadius = 0 // 展开后去掉圆角
        } completion: { finished in
            transitionContext.completeTransition(finished)
        }
    }
}

2. Dismiss动画类(缩回原Cell位置)

class DismissAnimation: NSObject, UIViewControllerAnimatedTransitioning {
    // 记录要缩回的目标位置(原Cell的frame)
    private let targetFrame: CGRect
    
    init(targetFrame: CGRect) {
        self.targetFrame = targetFrame
        super.init()
    }
    
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.35
    }
    
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let currentVC = transitionContext.viewController(forKey: .from) as? WishlistViewController,
              let container = transitionContext.containerView else { return }
        
        // 动画缩回原Cell位置,恢复圆角
        UIView.animate(withDuration: transitionDuration(using: transitionContext),
                       delay: 0,
                       options: .curveEaseIn) {
            currentVC.view.frame = self.targetFrame
            currentVC.view.layer.cornerRadius = 16
        } completion: { finished in
            currentVC.view.removeFromSuperview()
            transitionContext.completeTransition(finished)
        }
    }
}

二、给你的CollectionVC添加转场代理

在你的CollectionView所在的ViewController里实现UIViewControllerTransitioningDelegate,用来衔接动画:

extension YourCollectionVC: UIViewControllerTransitioningDelegate {
    // 提供present时的动画
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        guard let cell = collectionView.cellForItem(at: IndexPath(item: currentWishListIDX, section: 0)) else { return nil }
        // 把Cell的frame转换成屏幕坐标系
        let originFrame = collectionView.convert(cell.frame, to: nil)
        return PresentAnimation(originFrame: originFrame)
    }
    
    // 提供dismiss时的动画
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        guard let cell = collectionView.cellForItem(at: IndexPath(item: currentWishListIDX, section: 0)) else { return nil }
        let targetFrame = collectionView.convert(cell.frame, to: nil)
        return DismissAnimation(targetFrame: targetFrame)
    }
}

然后修改你点击Cell后的present代码,启用自定义转场:

cell.customWishlistTapCallback = {
    self.currentWishListIDX = indexPath.item
    let vc = self.storyboard?.instantiateViewController(withIdentifier: "WishlistVC") as! WishlistViewController
    vc.wishList = self.dataSourceArray[self.currentWishListIDX]
    // 关键:设置转场代理和自定义Presentation样式
    vc.transitioningDelegate = self
    vc.modalPresentationStyle = .custom
    self.present(vc, animated: true, completion: nil)
}

三、添加拖拽关闭功能

给WishlistViewController添加拖拽手势,结合交互转场控制器实现拖拽关闭:

class WishlistViewController: UIViewController {
    // 用来控制拖拽转场的进度
    private var interactionController: UIPercentDrivenInteractiveTransition?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setupPanGesture()
    }
    
    private func setupPanGesture() {
        let pan = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        view.addGestureRecognizer(pan)
    }
    
    @objc private func handlePanGesture(_ gesture: UIPanGestureRecognizer) {
        let translation = gesture.translation(in: view)
        // 计算拖拽进度(向下拖拽的距离占屏幕高度的比例)
        let progress = translation.y / view.bounds.height
        
        switch gesture.state {
        case .began:
            // 开始拖拽时初始化交互控制器,触发dismiss
            interactionController = UIPercentDrivenInteractiveTransition()
            dismiss(animated: true, completion: nil)
        case .changed:
            // 实时更新转场进度
            interactionController?.update(progress)
        case .ended, .cancelled:
            // 判断是否完成关闭:进度超过50%或拖拽速度够快就完成,否则回弹
            let velocity = gesture.velocity(in: view)
            if progress > 0.5 || velocity.y > 500 {
                interactionController?.finish()
            } else {
                interactionController?.cancel()
            }
            interactionController = nil
        default:
            break
        }
    }
    
    // 告诉系统使用我们的交互控制器处理拖拽转场
    func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return interactionController
    }
}

最后回到你的CollectionVC的转场代理扩展,添加交互控制器的支持:

extension YourCollectionVC: UIViewControllerTransitioningDelegate {
    // ... 之前的present/dismiss动画方法
    
    func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        guard let wishlistVC = presentedViewController as? WishlistViewController else { return nil }
        return wishlistVC.interactionController
    }
}

一些细节调整建议

  • 确保你的Cell和WishlistViewController的初始圆角一致,动画衔接会更自然
  • 可以根据N26的实际动画微调动画时长、弹簧参数(usingSpringWithDamping和initialSpringVelocity)
  • 如果你的CollectionView支持滚动,在dismiss前最好重新获取Cell的当前frame(避免Cell滚动后位置变化导致动画错位)

这样就能实现和N26一模一样的转场效果,同时支持拖拽关闭啦!

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.06 08:48:17