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");
}
控制臺輸出!:
多層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的導航控制器
重新閱讀presentingView
和presentedView
屬性中給到的注釋:
// 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
UIViewController
class 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中的存儲選項操作表單
任務目的要清晰。提供清晰標題或描述,幫助用戶理解當前位置和任務目標
避免同時展示多個模態視圖。多模態視圖會造成視覺混亂和認知負擔,尤其是模態視圖疊加時。提醒雖可顯示在其他內容之上,但不應同時顯示多個
參考文獻:
- https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/PresentingaViewController.html#//apple_ref/doc/uid/TP40007457-CH14-SW1
- https://developer.apple.com/cn/design/human-interface-guidelines/modality
- https://www.jianshu.com/p/dd6180bc340a
- https://juejin.cn/post/6844903708799533063#heading-4