1. 程式人生 > IOS開發 >iOS 聊聊present和dismiss

iOS 聊聊present和dismiss

前段時間遇到一個崩潰,最後發現是因為presentViewController彈了一個模態檢視導致的。今天就總結一下關於present和dismiss相關的問題。

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

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

  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,會有什麼樣的結果?

下文將逐個解答。

問題2:presentingViewController和presentedViewController屬性

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

//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,readonly) UIViewController *presentingViewController NS_AVAILABLE_IOS(5_0); 複製程式碼

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

---------------------A彈B後---------------------
A <ViewController: 0x7fe43ff0c9f0>
B <UIViewController: 0x7fe43ff05160>
A.presentingViewController (null)
A.presentedViewController <UIViewController: 0x7fe43ff05160>
B.presentingViewController <ViewController: 0x7fe43ff0c9f0>
B.presentedViewController (null)
---------------------B彈C後---------------------
C <UIViewController: 0x7fe43fd06190>
A.presentingViewController (null)
A.presentedViewController <UIViewController: 0x7fe43ff05160>
B.presentingViewController <ViewController: 0x7fe43ff0c9f0>
B.presentedViewController <UIViewController: 0x7fe43fd06190>
C.presentingViewController <UIViewController: 0x7fe43ff05160>
C.presentedViewController (null)

翻譯一下

---------------------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是頂層,沒有子節點

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

如果A已經彈了B,這個時候想要在彈一個C,正確的做法是,B彈C。

如果你嘗試用A彈C,系統會丟擲警告,並且介面不會有變化,即C不會被彈出,警告如下:

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

把警告內容翻譯一下,
"Warning: Attempt to present C on A which is already presenting B"

再翻譯一下,
"嘗試在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];
}
複製程式碼

或者這樣的

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

上述程式碼都會失敗,B並不會彈出,並會丟擲上面的警告。警告說得很明確,self.view還沒有被新增到檢視樹(父檢視),不允許彈出檢視。
也就是說,如果一個viewController的view還沒被新增到檢視樹(父檢視)上,那麼用這個viewController去present會失敗,並丟擲警告。

理論上,不應該建立一個UIViewController時就present另一個UIViewController。你可以用新增子檢視、子控制器的方式來實現類似效果(推薦)。

- (void)viewDidLoad {
    [super viewDidLoad];
 
    _BViewController = [[UIViewController alloc] init];
    _BViewController.view.frame = self.view.bounds;
    [self.view addSubview:_BViewController.view];
    [self addChildViewController:_BViewController];  //這句話一定要加,否則檢視上的按鈕事件可能不響應
}
複製程式碼

如果你非要這麼寫的話,可以把present的部分放到-viewDidAppear方法中,因為-viewDidAppear被呼叫時self.view已經被新增到檢視樹中了。

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    _BViewController = [[UIViewController alloc] init];
    [self presentViewController:_BViewController animated:NO completion:nil];
}
複製程式碼

關於UIView的生命週期,viewDidLoad系列方法的呼叫順序,可以參考這篇博文,寫得非常好。UIView生命週期詳解

如果覺得這篇文章對你有幫助,請點個贊吧。

作為一個開發者,有一個學習的氛圍和一個交流圈子特別重要,這是我的交流群761407670(123),大家有興趣可以進群裡一起交流學習


原文作者:CocoaKier

連結:https://www.jianshu.com/p/455d5f0b3656