如何实现类似WhatsApp消息页的Swipe To Push View Controller交互效果
实现方案
你之前用普通swipe/pan手势直接修改视图frame没达到原生效果的核心原因是没有使用系统提供的交互式自定义转场能力,手动控制的动画曲线、阻尼、过渡衔接都无法匹配系统原生动画的手感,最优实现思路如下:
- 自定义转场动画控制器
遵守UIViewControllerAnimatedTransitioning协议,在协议方法内实现push阶段的动画逻辑:新的ViewController从屏幕右侧滑入时,原有ViewController同步向左偏移30%屏幕宽度,同时透明度降到0.8,动画时长设为0.3s,采用系统自带的UIViewAnimationOptionCurveEaseInOut曲线,阻尼系数配置为0.9,完全对齐WhatsApp的动画效果。 - 绑定交互式转场控制
用系统提供的UIPercentDrivenInteractiveTransition作为交互控制器,和你添加在源视图上的平移手势绑定,根据手势位移计算转场进度:- 手势开始时触发push操作
- 手势移动过程中调用
updateInteractiveTransition:实时更新转场进度 - 手势结束时判断滑动距离超过屏幕宽度30%、且滑动速度符合要求则调用
finishInteractiveTransition完成转场,否则调用cancelInteractiveTransition取消转场,系统会自动完成动画的衔接过渡,不会出现生硬的跳变效果。
- 手势冲突优化
如果源视图是列表(UITableView/UICollectionView),实现UIGestureRecognizerDelegate的gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:方法,允许转场手势和列表滚动手势同时识别,同时添加判定规则:只有横向滑动距离大于纵向滑动距离2倍时才触发转场逻辑,避免和列表上下滚动操作冲突。 - 边缘触发适配
如果需要实现仅屏幕边缘滑动触发的效果,可以将普通UIPanGestureRecognizer替换为UIScreenEdgePanGestureRecognizer,指定触发边缘,优先级设置为高于列表手势,大幅降低误触概率。
核心代码示例
动画控制器核心实现(Objective-C)
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext { return 0.3; } - (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext { UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey]; UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey]; UIView *containerView = transitionContext.containerView; CGFloat screenWidth = CGRectGetWidth([UIScreen mainScreen].bounds); toVC.view.frame = CGRectMake(screenWidth, 0, screenWidth, CGRectGetHeight(containerView.bounds)); [containerView addSubview:toVC.view]; [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0 usingSpringWithDamping:0.9 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ toVC.view.frame = CGRectMake(0, 0, screenWidth, CGRectGetHeight(containerView.bounds)); fromVC.view.frame = CGRectMake(-screenWidth * 0.3, 0, screenWidth, CGRectGetHeight(containerView.bounds)); fromVC.view.alpha = 0.8; } completion:^(BOOL finished) { fromVC.view.alpha = 1.0; fromVC.view.frame = containerView.bounds; [transitionContext completeTransition:!transitionContext.transitionWasCancelled]; }]; }
手势处理核心实现(Swift)
@objc private func handlePan(_ gesture: UIPanGestureRecognizer) { let translationX = gesture.translation(in: view).x let progress = min(max(abs(translationX / view.bounds.width), 0), 1) switch gesture.state { case .began: interactiveTransition = UIPercentDrivenInteractiveTransition() navigationController?.pushViewController(targetViewController, animated: true) case .changed: interactiveTransition.update(progress) case .ended, .cancelled: let velocityX = gesture.velocity(in: view).x if progress > 0.3 || velocityX > 500 { interactiveTransition.finish() } else { interactiveTransition.cancel() } interactiveTransition = nil default: break } }
注意事项
- 所有动画逻辑都要放到自定义动画控制器中实现,不要直接在手势回调中修改视图frame,系统会自动处理转场过程中的视图层级、手势中断衔接等逻辑,最终效果和系统原生转场完全一致。
内容的提问来源于stack exchange,提问作者Mahmoud Shurrab
相关产品推荐
相关产品推荐

