前段時間遇到一個崩潰,最後發現是由於presentViewController彈了一個模態視圖致使的。今天就總結一下關於present和dismiss相關的問題。swift
假設有3個UIViewController,分別是A、B、C。下文中的「A彈B」是指[A presentViewController:B animated:NO completion:nil];
bash
下文將逐個解答。app
咱們先看看問題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 <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)ui
從上面的結果能夠得出,presentingViewController屬性返回父節點,presentedViewController屬性返回子節點,若是沒有父節點或子節點,返回nil。注意,這兩個屬性返回的是當前節點直接相鄰父子節點,並非返回最底層或者最頂層的節點(這點和文檔註釋有出入)。下面對照例子解釋下這個結論。this
---------------------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是頂層,沒有子節點atom
若是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。
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