iOS开发基础31-视图控制器转场深度解析:Push 与 Modal、模态样式、自定义转场与 Popover
视图控制器转场深度解析:Push 与 Modal、模态样式、自定义转场与 Popover
视图控制器转场是 iOS 界面导航的核心。本文系统梳理 Push(导航推送)与 Modal(模态弹出)两种转场方式的用法、适用场景、模态展示样式与转场动画、dismiss 机制、Popover 弹出、自定义转场,以及两种转场的生命周期差异。
一、Push 转场
1. 概述
Push 转场基于 UINavigationController,将新控制器压入导航栈并显示,原控制器保留在栈中。导航控制器自动在导航栏左侧提供返回按钮,用户可返回上一级。
2. 适用场景
- 层级结构:具有层级关系的页面,如设置页 → 子设置页、列表 → 详情。
- 需要返回:用户需要逐级返回的场景。
- 导航栏统一:需要统一的导航栏标题和返回按钮。
3. 基本用法
初始化导航控制器
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootVC = [[UIViewController alloc] init];
rootVC.view.backgroundColor = [UIColor whiteColor];
rootVC.title = @"Root";
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:rootVC];
self.window.rootViewController = nav;
[self.window makeKeyAndVisible];
return YES;
}
Push 新控制器
- (void)pushNext {
UIViewController *nextVC = [[UIViewController alloc] init];
nextVC.view.backgroundColor = [UIColor lightGrayColor];
nextVC.title = @"Next";
[self.navigationController pushViewController:nextVC animated:YES];
}
返回操作
// 返回上一级
[self.navigationController popViewControllerAnimated:YES];
// 返回到根控制器
[self.navigationController popToRootViewControllerAnimated:YES];
// 返回到指定控制器
[self.navigationController popToViewController:targetVC animated:YES];
4. 侧滑返回(interactivePopGestureRecognizer)
UINavigationController 自带从屏幕左边缘右滑返回的手势:
// 禁用侧滑返回(如首页不允许侧滑)
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
// 自定义手势代理,解决与其他手势的冲突
self.navigationController.interactivePopGestureRecognizer.delegate = self;
当自定义导航栏返回按钮或隐藏导航栏后,侧滑返回可能失效,需手动设置
interactivePopGestureRecognizer.delegate = self并实现代理方法恢复。
二、Modal 转场
1. 概述
Modal 转场以模态方式覆盖当前控制器,新控制器不加入导航栈。用户必须先完成模态页面的操作(如登录、选择)并关闭,才能回到原页面。
2. 适用场景
- 独立任务:需要用户专注完成的任务,如登录、编辑、拍照选择。
- 临时展示:临时弹出的页面,如分享面板、筛选条件。
- 打断流程:需要用户决策后才能继续的场景。
3. 基本用法
// 模态弹出
- (void)presentModal {
UIViewController *modalVC = [[UIViewController alloc] init];
modalVC.view.backgroundColor = [UIColor lightGrayColor];
modalVC.modalPresentationStyle = UIModalPresentationFullScreen;
[self presentViewController:modalVC animated:YES completion:nil];
}
// 关闭模态
- (void)closeModal {
[self dismissViewControllerAnimated:YES completion:nil];
}
4. 模态展示样式(modalPresentationStyle)
| 样式 | 说明 |
|---|---|
UIModalPresentationFullScreen |
全屏覆盖,原控制器从视图层级移除 |
UIModalPresentationPageSheet |
页面样式(iPad 宽度固定居中,iPhone 等同全屏) |
UIModalPresentationFormSheet |
表单样式(iPad 居中小于屏幕,iPhone 等同全屏) |
UIModalPresentationCurrentContext |
在当前上下文中展示(父控制器上下文) |
UIModalPresentationOverFullScreen |
覆盖全屏,原控制器保留在视图层级中(背景透明时可见下层) |
UIModalPresentationOverCurrentContext |
覆盖当前上下文,原控制器保留 |
UIModalPresentationPopover |
气泡弹出(iPad,iPhone 需适配) |
UIModalPresentationCustom |
自定义转场 |
UIModalPresentationAutomatic |
自动选择(iOS 13+ 默认值,iPhone 上通常为 pageSheet) |
UIModalPresentationNone |
不展示 |
iOS 13 重要变化:
modalPresentationStyle默认值从FullScreen改为Automatic(iPhone 上表现为 pageSheet,顶部露出原控制器一部分,可下拉关闭)。如需全屏必须显式设置UIModalPresentationFullScreen。
5. 转场动画样式(modalTransitionStyle)
typedef NS_ENUM(NSInteger, UIModalTransitionStyle) {
UIModalTransitionStyleCoverVertical = 0, // 从底部滑入(默认)
UIModalTransitionStyleFlipHorizontal, // 水平翻转
UIModalTransitionStyleCrossDissolve, // 淡入淡出
UIModalTransitionStylePartialCurl, // 翻页效果(仅 FullScreen 样式)
};
modalVC.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
6. dismiss 的调用者
dismissViewControllerAnimated:completion: 可由以下两方调用:
- presentingViewController(弹出者):推荐调用方。
- presentedViewController(被弹出者):调用时系统自动转发给 presentingViewController。
// 在被弹出的控制器中关闭(自动转发)
[self dismissViewControllerAnimated:YES completion:nil];
// 在弹出者中关闭(推荐)
[self dismissViewControllerAnimated:YES completion:^{
NSLog(@"已关闭");
}];
7. presentingViewController 与 presentedViewController
// 在被弹出的控制器中,访问弹出者
UIViewController *presenter = self.presentingViewController;
// 在弹出者中,访问被弹出的控制器
UIViewController *presented = self.presentedViewController;
判断当前控制器是否是模态弹出的,不能仅靠
presentingViewController != nil——如果导航控制器本身是模态弹出的,其每个子控制器的 presentingViewController 都不为 nil。更可靠的方式是通过self.navigationController.presentingViewController或在初始化时传入标记。
三、Popover 弹出
UIModalPresentationPopover 用于 iPad 上的气泡弹出(iPhone 上默认会转为全屏,需通过代理适配)。
- (void)presentPopoverFromButton:(UIButton *)sender {
UIViewController *popoverVC = [[UIViewController alloc] init];
popoverVC.modalPresentationStyle = UIModalPresentationPopover;
popoverVC.preferredContentSize = CGSizeMake(200, 300);
UIPopoverPresentationController *popPC = popoverVC.popoverPresentationController;
popPC.sourceView = sender;
popPC.sourceRect = sender.bounds;
popPC.permittedArrowDirections = UIPopoverArrowDirectionAny;
popPC.delegate = self;
[self presentViewController:popoverVC animated:YES completion:nil];
}
// iPhone 上适配为 Popover(而非全屏)
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller {
return UIModalPresentationNone; // 返回 None 表示不适应,保持 Popover 样式
}
四、自定义转场
通过 UIViewControllerTransitioningDelegate 和 UIViewControllerAnimatedTransitioning 实现完全自定义的转场动画。
1. 配置
@interface CustomModalVC : UIViewController <UIViewControllerTransitioningDelegate>
@end
@implementation CustomModalVC
- (instancetype)init {
if (self = [super init]) {
self.modalPresentationStyle = UIModalPresentationCustom;
self.transitioningDelegate = self;
}
return self;
}
// 返回 present 动画对象
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
return [[CustomTransitionAnimator alloc] init];
}
// 返回 dismiss 动画对象
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return [[CustomTransitionAnimator alloc] init];
}
@end
2. 动画对象
@interface CustomTransitionAnimator : NSObject <UIViewControllerAnimatedTransitioning>
@end
@implementation CustomTransitionAnimator
// 转场时长
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext {
return 0.5;
}
// 转场动画
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *container = [transitionContext containerView];
toVC.view.frame = container.bounds;
toVC.view.alpha = 0;
[container addSubview:toVC.view];
[UIView animateWithDuration:0.5 animations:^{
toVC.view.alpha = 1;
} completion:^(BOOL finished) {
[transitionContext completeTransition:YES];
}];
}
@end
五、Push 与 Modal 对比
| 特性 | Push 转场 | Modal 转场 |
|---|---|---|
| 依赖 | 需要 UINavigationController | 不需要 |
| 导航栈 | 加入导航栈 | 不加入导航栈 |
| 返回方式 | 导航栏返回按钮 / pop | 手动 dismiss |
| 导航栏 | 有(继承自导航控制器) | 无(需自己加) |
| 适用场景 | 层级导航、列表→详情 | 独立任务、登录、临时弹窗 |
| 原控制器状态 | 保留在栈中,不销毁 | FullScreen 时从视图层级移除,OverFullScreen 时保留 |
| 转场动画 | 从右侧推入(默认) | 从底部滑入(默认),可自定义 |
| 生命周期 | viewWillDisappear/viewDidDisappear | viewWillDisappear/viewDidDisappear(FullScreen) |
生命周期调用差异
- Push:原控制器调用
viewWillDisappear:/viewDidDisappear:,新控制器调用viewWillAppear:/viewDidAppear:。 - Modal(FullScreen):原控制器调用
viewWillDisappear:/viewDidDisappear:,新控制器调用viewWillAppear:/viewDidAppear:。 - Modal(OverFullScreen):原控制器不调用
viewWillDisappear:/viewDidDisappear:(因为原控制器视图仍保留在下层)。
六、综合示例
#pragma mark - RootViewController
@interface RootViewController ()
@end
@implementation RootViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
self.title = @"Root";
UIButton *pushBtn = [UIButton buttonWithType:UIButtonTypeSystem];
[pushBtn setTitle:@"Push" forState:UIControlStateNormal];
pushBtn.frame = CGRectMake(100, 100, 200, 50);
[pushBtn addTarget:self action:@selector(pushTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:pushBtn];
UIButton *modalBtn = [UIButton buttonWithType:UIButtonTypeSystem];
[modalBtn setTitle:@"Modal (FullScreen)" forState:UIControlStateNormal];
modalBtn.frame = CGRectMake(100, 170, 200, 50);
[modalBtn addTarget:self action:@selector(modalTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:modalBtn];
UIButton *pageSheetBtn = [UIButton buttonWithType:UIButtonTypeSystem];
[pageSheetBtn setTitle:@"Modal (PageSheet)" forState:UIControlStateNormal];
pageSheetBtn.frame = CGRectMake(100, 240, 200, 50);
[pageSheetBtn addTarget:self action:@selector(pageSheetTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:pageSheetBtn];
}
- (void)pushTapped {
NextViewController *nextVC = [[NextViewController alloc] init];
[self.navigationController pushViewController:nextVC animated:YES];
}
- (void)modalTapped {
NextViewController *modalVC = [[NextViewController alloc] init];
modalVC.modalPresentationStyle = UIModalPresentationFullScreen;
modalVC.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentViewController:modalVC animated:YES completion:nil];
}
- (void)pageSheetTapped {
NextViewController *modalVC = [[NextViewController alloc] init];
modalVC.modalPresentationStyle = UIModalPresentationPageSheet; // iOS 13+ 默认样式
[self presentViewController:modalVC animated:YES completion:nil];
}
@end
#pragma mark - NextViewController
@interface NextViewController ()
@property (nonatomic, assign) BOOL isModal; // 通过属性标记是否模态弹出
@end
@implementation NextViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor lightGrayColor];
self.title = @"Next";
if (self.isModal) {
UIButton *closeBtn = [UIButton buttonWithType:UIButtonTypeSystem];
[closeBtn setTitle:@"Close" forState:UIControlStateNormal];
closeBtn.frame = CGRectMake(100, 100, 100, 50);
[closeBtn addTarget:self action:@selector(closeTapped) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:closeBtn];
}
}
- (void)closeTapped {
[self dismissViewControllerAnimated:YES completion:nil];
}
@end
七、Swift 版本对照
// Push
let nextVC = NextViewController()
navigationController?.pushViewController(nextVC, animated: true)
// Pop
navigationController?.popViewController(animated: true)
navigationController?.popToRootViewController(animated: true)
navigationController?.popToViewController(targetVC, animated: true)
// 侧滑返回
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
// Modal
let modalVC = ModalViewController()
modalVC.modalPresentationStyle = .fullScreen // 全屏
modalVC.modalPresentationStyle = .pageSheet // 页面样式(iOS 13+ 默认)
modalVC.modalPresentationStyle = .overFullScreen // 覆盖全屏(背景透明)
modalVC.modalTransitionStyle = .coverVertical // 从底部滑入
modalVC.modalTransitionStyle = .crossDissolve // 淡入淡出
present(modalVC, animated: true)
// Dismiss
dismiss(animated: true)
// Popover
let popoverVC = PopoverViewController()
popoverVC.modalPresentationStyle = .popover
popoverVC.preferredContentSize = CGSize(width: 200, height: 300)
if let popPC = popoverVC.popoverPresentationController {
popPC.sourceView = button
popPC.sourceRect = button.bounds
popPC.permittedArrowDirections = .any
popPC.delegate = self
}
present(popoverVC, animated: true)
// 自定义转场
class CustomModalVC: UIViewController, UIViewControllerTransitioningDelegate {
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
modalPresentationStyle = .custom
transitioningDelegate = self
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomTransitionAnimator()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomTransitionAnimator()
}
}
八、总结
- Push 转场:基于 UINavigationController,将控制器压入导航栈,自动提供返回按钮,适用于层级导航;支持 pop/popToRoot/popToViewController 三种返回方式和侧滑返回手势。
- Modal 转场:模态覆盖当前控制器,不加入导航栈,需手动 dismiss;适用于独立任务和临时弹窗;通过 modalPresentationStyle 控制展示样式,modalTransitionStyle 控制转场动画。
- iOS 13 变化:modalPresentationStyle 默认从 FullScreen 改为 Automatic(iPhone 上为 pageSheet,可下拉关闭),全屏需显式设置 FullScreen。
- dismiss 机制:可由 presentingViewController 或 presentedViewController 调用,后者自动转发;通过 presentingViewController/presentedViewController 互相访问。
- Popover:iPad 气泡弹出,通过 popoverPresentationController 配置源视图和箭头方向;iPhone 上需通过 adaptivePresentationStyle 代理适配。
- 自定义转场:modalPresentationStyle = Custom + transitioningDelegate + UIViewControllerAnimatedTransitioning 动画对象。
- 生命周期差异:OverFullScreen 样式下原控制器不调用 viewWillDisappear/viewDidDisappear;FullScreen 和 Push 都会调用。

浙公网安备 33010602011771号