当前位置: 首页 > news >正文

push pop 和 present dismiss

push/pop 和 present/dismiss

文章目录

  • push/pop 和 present/dismiss
  • 前言
  • push / pop
  • present
    • 普通的present
    • 多层present
      • 多层present后的父子关系问题
      • 多层弹出会遇到的问题
    • showViewController 和 showDetailViewController
      • showViewController
      • showDetailViewController
  • dismiss
  • 模态化

前言

之前曾经在网易云仿写总结中,使用过present制作抽屉视图的展示,在这里再次详细对比一下push和present的区别

想象一下你正在看一本书,随着你读书的进度不断增加,你的页数也越来越多,这就是导航栏带来的逐层深入。

突然,你需要查一个重要的词语解释。你会从桌上拿起一本字典,把它放在你正在看的书页上,然后开始查找。这本字典就是你的模态视图,它暂时遮住了你正在看的书页(在iOS13之后,present出的页面的模态样式变为了UIModelPresentationAutomatic,新的页面将不再完全覆盖之前的页面,而只会部分遮挡,并且允许你向下拖动关闭)

push / pop

push的操作必须在一个UINavigationController的上下文中进行,导航控制器会维护一个栈,当push后,一个新的view会被压入到栈顶,同时导航控制器会自动添加一个按钮用于返回

因为push的弹出新界面是由于视图栈的顶部有更换,所以你对视图的添加和删除要完全符合栈的操作

你可以pop到指定视图,苹果也提供了相关方法(实际上用户只要长按返回按钮就可以返回到之前的指定页面)

请添加图片描述

popToViewController:animated: pop到指定的界面

popToRootViewControllerAnimated: 一直pop到根视图停止

但是你无法只删除栈中的某一页面

在pop时,对应的新视图会先调用viewDidLoad 方法,然后是viewWillAppear 最后是viewDidAppear

present

普通的present

从iOS13开始,present为了适配卡片风格展示,模态默认样式变为UIModelpresentationAutomatic 新的视图为部分覆盖

present本质是在新的视图上重新呈现了一个视图,当前的视图只能通过自己dismiss来返回原来的视图,同时,新旧视图之间是有层级关系的

源码:

// The view controller that was presented by this view controller or its nearest ancestor.
@property(nullable, nonatomic,readonly) UIViewController *presentedViewController  API_AVAILABLE(ios(5.0));// The view controller that presented this view controller (or its farthest ancestor.)
@property(nullable, nonatomic,readonly) UIViewController *presentingViewController API_AVAILABLE(ios(5.0));

可以看出,两个属性都没有加入内存类的属性关键字,所以都为strong强引用

所以在present视图之后,父视图和子视图会形成循环强引用,只有在新的视图被dismiss之后,才会断开循环引用

- (void)presentBlueBlackThenRed {UIViewController *vcBlue = [[UIViewController alloc] init];vcBlue.view.backgroundColor = [UIColor systemBlueColor];NSLog(@"准备呈现蓝色视图...");[self presentViewController:vcBlue animated:YES completion:^{UIViewController *vcBlack = [[UIViewController alloc] init];vcBlack.view.backgroundColor = [UIColor blackColor];[vcBlue presentViewController:vcBlack animated:YES completion:^{UIViewController *vcRed = [[UIViewController alloc] init];vcRed.view.backgroundColor = [UIColor systemRedColor];[vcBlack presentViewController:vcRed animated:YES completion:nil];}];}];NSLog(@"vcBlue的父视图是否为self? %@", [vcBlue.presentingViewController isEqual:self] ? @"YES" : @"NO");NSLog(@"self的子视图是否为vcBlue? %@", [self.presentedViewController isEqual:vcBlue] ? @"YES" : @"NO");
}

控制台输出:

![外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传](https://img-home.csdnimg.cn/images/20230724024159.png?origin_url=push%2520pop%2520%25E5%2592%258C%2520present%2520dismiss%2520263fe9f60608806999fbf5c11eec8eb5%2FCle请添加图片描述
anShot_2025-09-03_at_20.29.252x.png&pos_id=img-yKpQm1sc-1756982820540)

多层present

在一个已经被present的视图中再次present一个视图,是多层的present

之前我们也讲过,present的主视图和新的视图是强引用,所以如果过多的present视图,可能会导致内存泄露等等问题

present出的视图,是模态视图,在什么时候使用模态视图会在后面讲

但是不管是什么情况,都不建议使用多层的present

多层present后的父子关系问题

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意看,这张图里,VCA present了 VCB,VCB present了 VCC

并且A带有了导航控制器

- (void)presentBlueBlackThenRed {UIViewController *vcBlue = [[UIViewController alloc] init];vcBlue.view.backgroundColor = [UIColor systemBlueColor];UINavigationController* blueNavi = [[UINavigationController alloc] initWithRootViewController:vcBlue];NSLog(@"准备呈现蓝色视图...");[self presentViewController:blueNavi animated:YES completion:^{NSLog(@"vcBlue的父视图是否为self? %@", [vcBlue.presentingViewController isEqual:self] ? @"YES" : @"NO");NSLog(@"self的子视图是否为vcBlue? %@", [self.presentedViewController isEqual:vcBlue] ? @"YES" : @"NO");UIViewController *vcBlack = [[UIViewController alloc] init];vcBlack.view.backgroundColor = [UIColor blackColor];[vcBlue presentViewController:vcBlack animated:YES completion:^{NSLog(@"vcBlack的父视图是否为vcBlue? %@", [vcBlack.presentingViewController isEqual:vcBlue] ? @"YES" : @"NO");NSLog(@"vcBlack的父视图是否为vcBlue的导航栏? %@", [vcBlack.presentingViewController isEqual:vcBlue.navigationController] ? @"YES" : @"NO");NSLog(@"vcblue的子视图是否为vcBlack? %@", [vcBlue.presentedViewController isEqual:vcBlack] ? @"YES" : @"NO");UIViewController *vcRed = [[UIViewController alloc] init];vcRed.view.backgroundColor = [UIColor systemRedColor];[vcBlack presentViewController:vcRed animated:YES completion:^{NSLog(@"vcRed的父视图是否为vcBlack? %@", [vcRed.presentingViewController isEqual:vcBlack] ? @"YES" : @"NO");NSLog(@"vcblack的子视图是否为vcRed? %@", [vcBlack.presentedViewController isEqual:vcRed] ? @"YES" : @"NO");}];}];}];[vcBlue dismissViewControllerAnimated:YES completion:nil];}

请问,B的presentingView(即父视图)是谁?

答案是A的导航控制器

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

重新阅读presentingViewpresentedView 属性中给到的注释:

// The view controller that was presented by this view controller or its nearest ancestor.
// The view controller that presented this view controller (or its farthest ancestor.)

// 由该视图控制器或其最近的祖先呈现的视图控制器。
// 展示此视图控制器(或其最远祖先)的视图控制器。

注释中提到“or its nearest/farthest ancestor”,意思是这两个属性的值不一定是和弹操作直接关联的视图控制器

Apple官方文档也显示

Support for presenting view controllers is built in to the UIViewControllerclass and is available to all view controller objects. You can present any view controller from any other view controller, although UIKit might reroute the request to a different view controller.

UIViewController类中内置了对显示视图控制器的支持,并且适用于所有视图控制器对象。您可以从任何其他视图控制器呈现任何视图控制器,尽管UIKit可能会将请求重定向到其他视图控制器。

所以实际上你的新视图会根据视图的层级关系来调整到底是谁去presentingView,谁来当新视图的presentingView

有的博客提到了UIKit会根据你的新页面的弹出方式来判断谁去当presentingView,即如果是全屏,UIKit会寻找父视图上是全屏的VC作为新视图的presentingView,但是我这里敲过例子后发现不会,并且即使设置了definesPresentationContext,视图层级也不会变化

- (void)presentBlueBlackThenRed {UIViewController *vcBlue = [[UIViewController alloc] init];vcBlue.view.backgroundColor = [UIColor systemBlueColor];UINavigationController* blueNavi = [[UINavigationController alloc] initWithRootViewController:vcBlue];NSLog(@"准备呈现蓝色视图...");// 推出导航控制器来让blue有自己的导航控制器,测试是否会由于导航控制器影响子视图的presentingView[self presentViewController:blueNavi animated:YES completion:^{NSLog(@"vcBlue的父视图是否为self? %@", [vcBlue.presentingViewController isEqual:self] ? @"YES" : @"NO");NSLog(@"self的子视图是否为vcBlue? %@", [self.presentedViewController isEqual:vcBlue] ? @"YES" : @"NO");UIViewController *vcBlack = [[UIViewController alloc] init];vcBlack.view.backgroundColor = [UIColor blackColor];// 这里进行修改// vcBlue.definesPresentationContext = YES;vcBlack.modalPresentationStyle = UIModalPresentationFullScreen;[vcBlue presentViewController:vcBlack animated:YES completion:^{NSLog(@"vcBlack的父视图是否为vcBlue? %@", [vcBlack.presentingViewController isEqual:vcBlue] ? @"YES" : @"NO");NSLog(@"vcBlack的父视图是否为vcBlue的导航栏? %@", [vcBlack.presentingViewController isEqual:vcBlue.navigationController] ? @"YES" : @"NO");NSLog(@"vcblue的子视图是否为vcBlack? %@", [vcBlue.presentedViewController isEqual:vcBlack] ? @"YES" : @"NO");UIViewController *vcRed = [[UIViewController alloc] init];vcRed.view.backgroundColor = [UIColor systemRedColor];[vcBlack presentViewController:vcRed animated:YES completion:^{NSLog(@"vcRed的父视图是否为vcBlack? %@", [vcRed.presentingViewController isEqual:vcBlack] ? @"YES" : @"NO");NSLog(@"vcblack的子视图是否为vcRed? %@", [vcBlack.presentedViewController isEqual:vcRed] ? @"YES" : @"NO");//                NSLog(@"推出");
//                [vcRed dismissViewControllerAnimated:YES completion:nil];
//                [vcRed.presentingViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];}];}];}];//    [vcBlue dismissViewControllerAnimated:YES completion:nil];}

这里我使用了全屏展示的样式,但是vcBlack的presentingView还是vcBlue

之后在设置definesPresentationContext属性为YES,并加入导航控制器后,vcBlack的presentingView也没有变为vcBlue,而是重新定位到了vcBlue的导航控制器上

这个地方确实找不到其他的博客了,所以我先放在这里,期望会的大佬帮忙解答一下,感谢!

多层弹出会遇到的问题

还是刚刚的例子

在A视图推出B视图,如果想呈现C视图,应该使用B视图来推出对吧

但是如果我使用A视图推出会发生什么呢?

- (void)presentBlueBlackThenRed {UIViewController *vcBlue = [[UIViewController alloc] init];vcBlue.view.backgroundColor = [UIColor systemBlueColor];UINavigationController* blueNavi = [[UINavigationController alloc] initWithRootViewController:vcBlue];NSLog(@"准备呈现蓝色视图...");// 推出导航控制器来让blue有自己的导航控制器,测试是否会由于导航控制器影响子视图的presentingView[self presentViewController:blueNavi animated:YES completion:^{NSLog(@"vcBlue的父视图是否为self? %@", [vcBlue.presentingViewController isEqual:self] ? @"YES" : @"NO");NSLog(@"self的子视图是否为vcBlue? %@", [self.presentedViewController isEqual:vcBlue] ? @"YES" : @"NO");UIViewController *vcBlack = [[UIViewController alloc] init];vcBlack.view.backgroundColor = [UIColor blackColor];// 这里进行修改vcBlue.definesPresentationContext = YES;
//        vcBlack.modalPresentationStyle = UIModalPresentationFullScreen;[vcBlue presentViewController:vcBlack animated:YES completion:^{NSLog(@"vcBlack的父视图是否为vcBlue? %@", [vcBlack.presentingViewController isEqual:vcBlue] ? @"YES" : @"NO");NSLog(@"vcBlack的父视图是否为vcBlue的导航栏? %@", [vcBlack.presentingViewController isEqual:vcBlue.navigationController] ? @"YES" : @"NO");NSLog(@"vcblue的子视图是否为vcBlack? %@", [vcBlue.presentedViewController isEqual:vcBlack] ? @"YES" : @"NO");UIViewController *vcRed = [[UIViewController alloc] init];vcRed.view.backgroundColor = [UIColor systemRedColor];vcRed.modalPresentationStyle = UIModalPresentationFullScreen;[vcBlue presentViewController:vcRed animated:YES completion:^{NSLog(@"vcRed的父视图是否为vcBlack? %@", [vcRed.presentingViewController isEqual:vcBlack] ? @"YES" : @"NO");NSLog(@"vcblack的子视图是否为vcRed? %@", [vcBlack.presentedViewController isEqual:vcRed] ? @"YES" : @"NO");//                NSLog(@"推出");
//                [vcRed dismissViewControllerAnimated:YES completion:nil];
//                [vcRed.presentingViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];}];}];}];//    [vcBlue dismissViewControllerAnimated:YES completion:nil];}

控制台输出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

并且红色视图不会被弹出

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

使用present去弹模态视图的时候,只能用最顶层的的控制器去弹,用底层的控制器去弹会失败,并抛出警告

(当然是有邪修方法的)

如果一个viewController的view还没被添加到视图树(父视图)上,那么用这个viewController去present会失败,并抛出警告。

如果你非要这么写的话,可以把present的部分放到-viewDidAppear方法中,因为-viewDidAppear被调用时self.view已经被添加到视图树中了(强烈不推荐)

正确的做法应该是使用childViewController,你可以用添加子视图、子控制器的方式来实现类似效果

- (void)viewDidLoad {[super viewDidLoad];_BViewController = [[UIViewController alloc] init];_BViewController.view.frame = self.view.bounds;[self.view addSubview:_BViewController.view];[self addChildViewController:_BViewController];  //这句话一定要加,否则视图上的按钮事件可能不响应
}

showViewController 和 showDetailViewController

在阅读苹果的《Presenting a View Controller》时,找到了这两个方法,在这里进行简单的区分

showViewController

showViewController:sender: 这个方法,可以说是 UIKit 框架为了适应不同尺寸屏幕(尤其是为了 iPad)而设计的一个“智能”导航方法,它和 presentViewController: 的区别,就像是智能导航指定路线的区别

presentViewController: 这个方法就是在对系统下达死命令,不管怎么样,你的视图必须以模态视图的方法弹出,它的行为是固定的,结果是可预期的

showViewController:sender: 这个方法不是在下达死命令,而是在向系统阐述一个需求:我希望把这个视图控制器展示给用户,请你根据当前的环境,用最合适的方式把它显示出来

这个“当前的环境”是什么呢?主要是指当前的视图控制器层级

  • 如果当前 VC 在 UINavigationController 里:系统会认为“最合适”的方式是 push。所以 showViewController: 的效果就等同于 [self.navigationController pushViewController:vc animated:YES]
  • 如果当前 VC 不在 UINavigationController 里:系统找不到可以 push 的“轨道”,就会认为“最合适”的方式是 present。所以 showViewController: 的效果就等同于 [self presentViewController:vc animated:YES completion:nil]
  • 在 iPad 的 SplitViewController 环境下:情况会更复杂。如果你的 App 在分屏模式下运行,showViewController: 可能会在一个窗格里 push 一个视图,也可能会在另一个窗格里 present一个视图,完全取决于 Split View Controller 的当前状态和结构。它会自动适应,选择最符合 iPadOS 人机交互规范的方式

showDetailViewController

showDetailViewController: 是一个**专门用于“主从式”界面的、目的性更强的“智能导航”,**它的用处,几乎完全绑定在 UISplitViewController 这种视图容器上

它所表达的意思就是,把这个视图控制器,显示在‘详情’区域!

这个基本上只用于ipad,所以这里就不展开描述了,只做了解即可

dismiss

首先需要明确一个事情,A弹出B,然后在B中执行dismissViewControllerAnimated:completion: 这个很常见的流程实际上是错的

之所以我们可以这么做,实际上是因为UIKit会帮我们自动通知父视图dismiss

一般,大家也都是这么用的,A弹B,B中调用dismiss消失弹框。没问题。

那,A弹B,我在A中调用dismiss可以吗?——也没问题,B会消失。

那,A弹B,B弹C。A调用dismiss,会有什么样的结果?是C消失,还是B、C都消失,还是会报错? ——正确答案是B、C都消失

如果B掉dismiss呢?B会消失吗?——答案是:只有C会消失,B不会消失

还是上面的代码

- (void)presentBlueBlackThenRed {UIViewController *vcBlue = [[UIViewController alloc] init];vcBlue.view.backgroundColor = [UIColor systemBlueColor];UINavigationController* blueNavi = [[UINavigationController alloc] initWithRootViewController:vcBlue];NSLog(@"准备呈现蓝色视图...");// 推出导航控制器来让blue有自己的导航控制器,测试是否会由于导航控制器影响子视图的presentingView[self presentViewController:blueNavi animated:YES completion:^{NSLog(@"vcBlue的父视图是否为self? %@", [vcBlue.presentingViewController isEqual:self] ? @"YES" : @"NO");NSLog(@"self的子视图是否为vcBlue? %@", [self.presentedViewController isEqual:vcBlue] ? @"YES" : @"NO");UIViewController *vcBlack = [[UIViewController alloc] init];vcBlack.view.backgroundColor = [UIColor blackColor];// 这里进行修改
//        vcBlue.definesPresentationContext = YES;
//        vcBlack.modalPresentationStyle = UIModalPresentationFullScreen;[vcBlue presentViewController:vcBlack animated:YES completion:^{NSLog(@"vcBlack的父视图是否为vcBlue? %@", [vcBlack.presentingViewController isEqual:vcBlue] ? @"YES" : @"NO");NSLog(@"vcBlack的父视图是否为vcBlue的导航栏? %@", [vcBlack.presentingViewController isEqual:vcBlue.navigationController] ? @"YES" : @"NO");NSLog(@"vcblue的子视图是否为vcBlack? %@", [vcBlue.presentedViewController isEqual:vcBlack] ? @"YES" : @"NO");UIViewController *vcRed = [[UIViewController alloc] init];vcRed.view.backgroundColor = [UIColor systemRedColor];vcRed.modalPresentationStyle = UIModalPresentationFullScreen;[vcBlack presentViewController:vcRed animated:YES completion:^{NSLog(@"vcRed的父视图是否为vcBlack? %@", [vcRed.presentingViewController isEqual:vcBlack] ? @"YES" : @"NO");NSLog(@"vcblack的子视图是否为vcRed? %@", [vcBlack.presentedViewController isEqual:vcRed] ? @"YES" : @"NO");NSLog(@"推出");
//                [vcRed dismissViewControllerAnimated:YES completion:nil];[vcRed.presentingViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];}];}];}];//    [vcBlue dismissViewControllerAnimated:YES completion:nil];}

我在弹出vcRed之后,用vcRed的presentingViewController的presentingViewController来进行dismiss操作

vcRed的presentingViewController是vcBlack,vcBlack的presentingViewController是vcBlue的导航控制器

按照理论来说,我们这里应该弹出全部的视图,也就是整个页面应该回到白色

但实际上:

在这里插入图片描述

可以观察到蓝色视图并没有弹出

阅读官方文档关于这个方法的解释:

To dismiss a presented view controller, call the dismissViewControllerAnimated:completion: method of the presenting view controller. You can also call this method on the presented view controller itself. When you call the method on the presented view controller, UIKit automatically forwards the request to the presenting view controller.

文档指出

1. 父节点负责调用dismiss来关闭他弹出来的子节点,你也可以直接在子节点中调用dismiss方法,UIKit会通知父节点去处理

2. 如果你连续弹出多个节点,应当由最底层的父节点调用dismiss来一次性关闭所有子节点;如果中间节点调dismiss,中间节点上面的节点都会消失,但中间几点本身并不会消失,这里要注意

3. 关闭多个子节点时,只有最顶层的子节点会有动画效果,下层的子节点会直接被移除,不会有动画效果

模态化

模态化是一种在单独的专用模式中呈现内容的设计技术,这种模式可阻止与父视图交互且需要进行确切的操作来关闭

以模态化方式呈现内容可以:

  • 确保用户接收关键信息,并在必要时对其进行操作
  • 提供选项以让用户确认或修改其最近的操作
  • 帮助用户执行明确的小范围任务,同时不会忘记其之前的环境
  • 向用户提供沉浸式体验,或帮助其专注于复杂任务

使用场景:

仅在必要时使用模态化。模态体验打断用户当前流程且需要额外操作关闭,因此只在需要用户专注或做出重要选择时使用

保持模态任务简短。复杂的模态任务可能导致用户忘记原任务,尤其是当模态视图完全覆盖原界面时

避免"App中的App"体验。若模态任务需要子视图,提供单一导航路径,避免使用可能被误认为关闭按钮的元素

复杂任务用全屏模态。全屏模态减少干扰,适用于视频、照片、相机或多步骤任务。

提供明显的关闭方式。遵循平台惯例:iOS/iPadOS/watchOS上通常在顶部工具栏或下滑手势,macOS和tvOS则在主视图中

关闭前避免数据丢失。若关闭可能丢失用户内容,提供解释和解决方案,如iOS中的存储选项操作表单

任务目的要清晰。提供清晰标题或描述,帮助用户理解当前位置和任务目标

避免同时展示多个模态视图。多模态视图会造成视觉混乱和认知负担,尤其是模态视图叠加时。提醒虽可显示在其他内容之上,但不应同时显示多个

参考文献:

  1. https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/PresentingaViewController.html#//apple_ref/doc/uid/TP40007457-CH14-SW1
  2. https://developer.apple.com/cn/design/human-interface-guidelines/modality
  3. https://www.jianshu.com/p/dd6180bc340a
  4. https://juejin.cn/post/6844903708799533063#heading-4
http://www.xdnf.cn/news/1460071.html

相关文章:

  • Leetcode 206. 反转链表 迭代/递归
  • 拦截器和过滤器(理论+实操)
  • Websocket链接如何配置nginx转发规则?
  • NV169NV200美光固态闪存NV182NV184
  • 云数据库服务(参考自腾讯云计算工程师认证课程)更新中......
  • 阿里云 ESA 实时log 发送没有quta的解决
  • 【机器学习】HanLP+Weka+Java=Random Forest算法模型
  • 【CS32L015C8T6】配置单片机时基TimeBase(内附完整代码及注释)
  • Mysql杂志(九)
  • [frontend]WebGL是啥?
  • AI入坑: Trae 通过http调用.net 开发的 mcp server
  • 批量生成角色及动画-统一角色为Mixamo骨骼(一)
  • Qt实现2048小游戏:看看AI如何评估棋盘策略实现“人机合一
  • 对于数据结构:链表的超详细保姆级解析
  • Java Thread线程2—线程锁synchronized,Lock,volatile
  • Python学习3.0使用Unittest框架运行测试用例
  • 无人机防风技术难点解析
  • TDengine TIMETRUNCATE 函数用户使用手册
  • Netty从0到1系列之Buffer【下】
  • 2025年百度商业AI技术创新大赛赛道二:视频广告生成推理性能优化-初赛第五名,复赛第九名方案分享
  • JVM 运行时数据区域
  • java面试中经常会问到的dubbo问题有哪些(基础版)
  • JVM 类加载全过程
  • Node-RED服务成本/价格很高?那这不到“三张”的怎么说?
  • QT卡顿的可能原因
  • TP8 数组在模板html文件中输出json字符串格式{“0“:“x1“,“1“:“x2“,“2“:“x3“}
  • 在Spring MVC中使用查询字符串与参数
  • 2025市面上比较实用的财会行业证书,最值得考的8个职业证书推荐
  • 本地部署开源数据生成器项目实战指南
  • HarmonyOS应用开发之界面列表不刷新问题Bug排查记:从现象到解决完整记录