由於種種原因,掘金等第三方平臺部落格不再保證能夠同步更新,歡迎移步 GitHub:github.com/kingcos/Per…。謝謝!
Lifecycle of UIViewController in iOS
Date | Notes | Swift | Xcode |
---|---|---|---|
2017-03-10 | 首次提交 | 3.0 | 8.2.1 |
前言
物件的生命週期一直是開發者所需要關心的,教授 CS193p 的老師 Paul 也詳細的講述了 UIViewController 的生命週期。為了記述這一過程,故作此文。由於 Xcode 提供了純程式碼和 Storyboard(Xib 同理)兩種佈局 UI 的方式,因此初始化部分略有不同。
為了方便觀察,我建立了一個 BaseViewController,繼承自原本的 UIViewController,重寫其中的生命週期方法,並讓後續新的控制器繼承自該控制器,以便觀察。
本文對應的 Demo 可以在 github.com/kingcos/UIV… 檢視、下載。
Initialization
Storyboard
OUTPUT: init(coder:) awakeFromNib()
init(coder:)
- 當使用 Storyboard 時,控制器的構造器為
init(coder:)
。 - 該構造器為必需構造器,如果重寫其他構造器,則必須重寫該構造器。
- 該構造器為可失敗構造器,即有可能構造失敗,返回 nil。
- 該方法來源自 NSCoding 協議,而 UIViewController 遵從這一協議。
- 該方法被呼叫意味著控制器有可能(並非一定)在未來會顯示。
- 在控制器生命週期中,該方法只會被呼叫一次。
awakeFromNib()
- 當使用 Storyboard 時,該方法會被呼叫。
- 當呼叫該方法時,將保證所有的 outlet 和 action 連線已經完成。
- 該方法內部必須呼叫父類該方法,雖然預設實現為空,但 UIKit 中許多類的該方法為非空。
- 由於控制器中物件的初始化順序不能確定,所以構造器中不應該向其他物件傳送訊息,而應當在
awakeFromNib()
中安全地傳送。 - 通常使用
awakeFromNib()
可以進行在設計時無法完成的必要額外設定。
Code
OUTPUT: init(nibName:bundle:) - NibName: nil, Bundle: nil
init(nibName:bundle:)
- 當使用純程式碼建立控制器,控制器的構造器為
init(nibName:bundle:)
。 - 雖然使用程式碼建立時呼叫了該構造器,但傳入的引數均為 nil。
OUTPUT: loadView() viewDidLoad() viewWillAppear viewWillLayoutSubviews() - Optional((162.0, 308.0, 50.0, 50.0)) viewDidLayoutSubviews() - Optional((67.0, 269.0, 241.0, 129.0)) viewDidAppear viewWillDisappear viewDidDisappear deinit
loadView()
loadView()
即載入控制器管理的 view。- 不能直接手動呼叫該方法;當 view 被請求卻為 nil 時,該方法載入並建立 view。
- 若控制器有關聯的 Nib 檔案,該方法會從 Nib 檔案中載入 view;如果沒有,則建立空白 UIView 物件。
- 如果使用 Interface Builder 建立 view,則務必不要重寫該方法。
- 可以使用該方法手動建立檢視,且需要將根檢視分配為 view;自定義實現不應該再呼叫父類的該方法。
- 執行其他初始化操作,建議放在
viewDidLoad()
中。
viewDidLoad()
- view 被載入到記憶體後呼叫
viewDidLoad()
。 - 重寫該方法需要首先呼叫父類該方法。
- 該方法中可以額外初始化控制元件,例如新增子控制元件,新增約束。
- 該方法被呼叫意味著控制器有可能(並非一定)在未來會顯示。
- 在控制器生命週期中,該方法只會被呼叫一次。
viewWillAppear(_:)
- 該方法在控制器 view 即將新增到檢視層次時以及展示 view 時所有動畫配置前被呼叫。
- 重寫該方法需要首先呼叫父類該方法。
- 該方法中可以進行操作即將顯示的 view,例如改變狀態列的取向,型別。
- 該方法被呼叫意味著控制器將一定會顯示。
- 在控制器生命週期中,該方法可能會被多次呼叫。
注意: 如果控制器 A 被展示在另一個控制器 B 的 popover 中,那麼控制器 B 不會呼叫該方法,直到控制器 A 清除。
viewWillLayoutSubviews()
- 該方法在通知控制器將要佈局 view 的子控制元件時呼叫。
- 每當檢視的 bounds 改變,view 將調整其子控制元件位置。
- 該方法可重寫以在 view 佈局子控制元件前做出改變。
- 該方法的預設實現為空。
- 該方法呼叫時,AutoLayout 未起作用。
- 在控制器生命週期中,該方法可能會被多次呼叫。
viewDidLayoutSubviews()
- 該方法在通知控制器已經佈局 view 的子控制元件時呼叫。
- 該方法可重寫以在 view 佈局子控制元件後做出改變。
- 該方法的預設實現為空。
- 該方法呼叫時,AutoLayout 已經完成。
- 在控制器生命週期中,該方法可能會被多次呼叫。
viewDidAppear(_:)
- 該方法在控制器 view 已經新增到檢視層次時被呼叫。
- 重寫該方法需要首先呼叫父類該方法。
- 該方法可重寫以進行有關正在展示的檢視操作。
- 在控制器生命週期中,該方法可能會被多次呼叫。
viewWillDisappear(_:)
- 該方法在控制器 view 將要從檢視層次移除時被呼叫。
- 類似
viewWillAppear(_:)
。 - 該方法可重寫以提交變更,取消檢視第一響應者狀態。
viewDidDisappear(_:)
- 該方法在控制器 view 已經從檢視層次移除時被呼叫。
- 類似
viewDidAppear(_:)
。 - 該方法可重寫以清除或隱藏控制元件。
didReceiveMemoryWarning()
- 當記憶體預警時,該方法被呼叫。
- 不能直接手動呼叫該方法。
- 該方法可重寫以釋放資源、記憶體。
deinit
- 控制器銷燬時(離開堆),呼叫該方法。
Note
Rotation
OUTPUT: willTransition(to:with:) viewWillLayoutSubviews() - Optional((67.5, 269.5, 240.0, 128.0)) viewDidLayoutSubviews() - Optional((213.5, 123.5, 240.0, 128.0)) viewWillLayoutSubviews() - Optional((213.5, 123.5, 240.0, 128.0)) viewDidLayoutSubviews() - Optional((213.5, 123.5, 240.0, 128.0)) viewWillLayoutSubviews() - Optional((213.5, 123.5, 240.0, 128.0)) viewDidLayoutSubviews() - Optional((213.5, 123.5, 240.0, 128.0))
- 當 view 轉變,會呼叫
willTransition(to:with:)
方法。 - 當螢幕旋轉,view 的 bounds 改變,其內部的子控制元件也需要按照約束調整為新的位置,因此也呼叫了
viewWillLayoutSubviews()
和viewDidLayoutSubviews()
。
Present & Dismiss
OUTPUT: viewWillDisappear viewDidDisappear viewDidDisappear viewWillAppear viewDidAppear
- 當在一個控制器內 Present 新的控制器,原先的控制器並不會銷燬,但會消失,因此呼叫了
viewWillDisappear
和viewDidDisappear
方法。 - 如果新的控制器 Dismiss,即清除自己,原先的控制器會再一次出現,因此呼叫了其中的
viewWillAppear
和viewDidAppear
方法。
死迴圈
class LoopViewController: UIViewController {
override func loadView() {
print(#function)
}
override func viewDidLoad() {
print(#function)
let _ = view
}
}
複製程式碼
OUTPUT: loadView() viewDidLoad() loadView() viewDidLoad() loadView() viewDidLoad() loadView() viewDidLoad() loadView()
- 若
loadView()
沒有載入 view,viewDidLoad()
會一直呼叫loadView()
載入 view,因此構成了死迴圈,程式即卡死。