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 都会调用。

posted @ 2015-08-03 23:47  Mr.陳  阅读(374)  评论(0)    收藏  举报