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
相关产品推荐
相关产品推荐

