iOS 聊聊present和dismiss

今天遇到一個崩潰,最後發現是由於present彈了一個模態視圖致使的。今天就總結一下關於present和dismiss相關的問題。swift

先列幾個問題,你能答上來嗎

假設有3個UIViewController,分別是A、B、C。下文中的「A彈B」是指
[A presentViewController:B animated:NO completion:nil];bash

  1. 若是A已經彈了B,這個時候你想在彈一個C,是應該A彈C,仍是B彈C,A彈C可不可行?
  2. 關於UIViewController的兩個屬性,presentingViewController和presentedViewController。
    若是A彈B,A.presentingViewController = ?,A.presentedViewController = ?,B.presentingViewController = ?,B.presentedViewController = ? 若是A彈B,B彈C呢?
  3. 若是A彈B,B彈C。A調用dismiss,會有什麼樣的結果?

下文將逐個解答。app

問題2:presentingViewController和presentedViewController屬性

咱們先看看問題2。UIViewController有兩個屬性,presentedViewController和presentingViewController。看文檔的註釋或許你能明白,反正樓主不太明白,明白了也容易忘記,記不住。iview

//UIKit.UIViewController.h
// The view controller that was presented by this view controller or its nearest ancestor.
@property(nullable, nonatomic,readonly) UIViewController *presentedViewController  NS_AVAILABLE_IOS(5_0);

// The view controller that presented this view controller (or its farthest ancestor.)
@property(nullable, nonatomic,readonly) UIViewController *presentingViewController NS_AVAILABLE_IOS(5_0);
複製代碼

那本身寫個Demo驗證一下唄:咱們建立A、B、C三個試圖控制器,上面分別放上按鈕,點A上的按鈕,A彈B,點B上的按鈕,B彈C。結束時分別打印各自的presentedViewController和presentingViewController屬性。結果以下:測試

---------------------A彈B後---------------------
 A.presentingViewController (null)     
 A.presentedViewController B          
 B.presentingViewController A
 B.presentedViewController (null)
---------------------B彈C後---------------------
 A.presentingViewController (null)
 A.presentedViewController B
 B.presentingViewController A
 B.presentedViewController C
 C.presentingViewController B
 C.presentedViewController (null)
複製代碼

從上面的結果能夠得出,presentingViewController屬性返回相鄰父節點,presentedViewController屬性返回相鄰子節點,若是沒有父節點或子節點,返回nil。注意,這兩個屬性返回的是當前節點直接相鄰父子節點,並非返回最底層或者最頂層的節點(這點和文檔註釋有出入)。下面對照例子解釋下這個結論。動畫

---------------------A彈B後---------------------
A.presentingViewController (null)      //由於A是最底層,沒有父節點,因此A的父節點返回nil
 A.presentedViewController B           //B在A的上層,B是A的子節點,因此A的子節點返回B
 B.presentingViewController A          //B的父節點是A,因此B的父節點返回A
 B.presentedViewController (null)      //B沒有子節點,因此B的子節點返回nil
---------------------B彈C後---------------------
 A.presentingViewController (null)     //A是最底層,沒有父節點
 A.presentedViewController B           //A的直接子節點是B
 B.presentingViewController A          //B的父節點是A
 B.presentedViewController C           //B的子節點是C
 C.presentingViewController B          //C的直接父節點是B
 C.presentedViewController (null)      //C是頂層,沒有子節點
複製代碼

子控制器childViewControllers

當一個控制器成爲另外一個控制器(常見的如,UINavigationViewController、UITabBarController、UIPageViewController等)的子控制器時,子控制器的presentedViewController和presentingViewController屬性由父控制器的presentedViewController和presentingViewController的屬性決定。ui

例如 A present Navi(B),那麼this

  1. B.presentingViewController = Navi.presentingViewController = A
  2. B.presentedViewController = Navi.presentedViewController = nil
  3. A.presentingViewController = nil
  4. A.presentedViewController = Navi

解釋一下上面的例子
假如,A和B是UIViewController,Navi是UINavigationController,B是Navi的rootViewController。atom

  1. 由於B是Navi的子控制器,根據剛纔的結論,子控制的presentingViewController和presentedViewController由父控制器決定,B的父控制器是Navi,Navi的presentingViewController是誰呢,根據以前的結論,presentingViewController屬性返回相鄰的父節點,即A。因此,B.presentingViewController = Navi.presentingViewController = A。
  2. 同理1.
  3. A沒有父控制器,A.presentingViewController等於A相鄰父節點,因爲A沒有父節點,因此A.presentingViewController = nil。
  4. A沒有父控制器,A.presentedViewController等於A相鄰子節點,A的子節點是Navi,因此A.presentedViewController = Navi。

問題1:present的層級問題,屢次彈窗由誰去彈

若是A已經彈了B,這個時候想要在彈一個C,正確的作法是,B彈C。spa

若是你嘗試用A彈C,系統會拋出警告,而且界面不會有變化,即C不會被彈出,警告以下:

Warning: Attempt to present <UIViewController: 0x7fbcecc04e80> on <ViewController: 0x7fbcecd09850> which is already presenting <UIViewController: 0x7fbcef2024c0>

把警告內容翻譯一下,
"嘗試在A上彈C,可是A已經彈了B"

這下就很清楚了,使用present去彈模態視圖的時候,只能用最頂層的的控制器去彈,用底層的控制器去彈會失敗,並拋出警告。

我簡單地寫了個方法來獲取傳入viewController的最頂層子節點,你們能夠參考下。

//獲取最頂層的彈出視圖,沒有子節點則返回自己
+ (UIViewController *)topestPresentedViewControllerForVC:(UIViewController *)viewController
{
    UIViewController *topestVC = viewController;
    while (topestVC.presentedViewController) {
        topestVC = topestVC.presentedViewController;
    }
    return topestVC;
}
複製代碼

一個崩潰問題

文章開頭我提到過一個崩潰問題,下面是崩潰時Xcode的日誌:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Application tried to present modally an active controller <ViewController: 0x7feddce0c9e0>.'

通過排查我發現,若是present一個已經被presented的視圖控制器就會崩潰,通常是不會出現這種情形的,若是出現了多是由於同一行present的代碼被屢次執行致使的,注意檢查你的代碼邏輯,修復bug

問題3:dismiss方法

dismiss方法你們都很熟悉吧
- (void)dismissViewControllerAnimated: (BOOL)flag completion: (void (^ __nullable)(void))completion
通常,你們都是這麼用的,A彈B,B中調用dismiss消失彈框。沒問題。
那,A彈B,我在A中調用dismiss能夠嗎?——也沒問題,B會消失。
那,A彈B,B彈C。A調用dismiss,會有什麼樣的結果?是C消失,仍是B、C都消失,仍是會報錯? ——正確答案是B、C都消失。

咱們來看下官方文檔對這個方法的說明。

The presenting view controller is responsible for dismissing the view controller it presented. If you call this method on the presented view controller itself, UIKit asks the presenting view controller to handle the dismissal. If you present several view controllers in succession, thus building a stack of presented view controllers, calling this method on a view controller lower in the stack dismisses its immediate child view controller and all view controllers above that child on the stack. When this happens, only the top-most view is dismissed in an animated fashion; any intermediate view controllers are simply removed from the stack. The top-most view is dismissed using its modal transition style, which may differ from the styles used by other view controllers lower in the stack.

文檔指出
1. 父節點負責調用dismiss來關閉他彈出來的子節點,你也能夠直接在子節點中調用dismiss方法,UIKit會通知父節點去處理。
2. 若是你連續彈出多個節點,應當由最底層的父節點調用dismiss來一次性關閉全部子節點。
3. 關閉多個子節點時,只有最頂層的子節點會有動畫效果,下層的子節點會直接被移除,不會有動畫效果。

通過個人測試,確實如此。

一個常見的錯誤

下面這個錯誤很容易遇到吧。

Warning: Attempt to present <UIViewController: 0x7fa43ac0bdb0> on <ViewController: 0x7fa43ae15de0> whose view is not in the window hierarchy!

你的代碼多是這樣的

- (void)viewDidLoad {
    [super viewDidLoad];
 
    _BViewController = [[UIViewController alloc] init];
    [self presentViewController:_BViewController animated:NO completion:nil];
}
複製代碼

上述代碼都會失敗,B並不會彈出,並會拋出上面的警告。警告說得很明確,self.view尚未被添加到視圖樹(父視圖),不容許彈出視圖。
也就是說,若是一個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];  //這句話必定要加,不然視圖上的按鈕事件可能不響應
}
複製代碼

關於UIView的生命週期,viewDidLoad系列方法的調用順序,能夠參考這篇博文,寫得很是好。UIView生命週期詳解

若是以爲這篇文章對你有幫助,請點個贊吧。若是有疑問能夠關注個人公衆號我留言。
轉載請註明出處,謝謝!

參考連接
你真的瞭解iOS中控制器的present和dismiss嗎?

相關文章
相關標籤/搜索