viewDidLoad()呼ばれるタイミングをちゃんとわかってなかった
UIViewController の viewDidLoad() 。
iOS開発をはじめて、最初にコードを書いたところではないでしょうか。
そんな基本中の基本である viewDidLoad() なんですが、呼ばれるタイミングを誤解していたことが先日判明したので、書きます。
サンプルコード
たとえばですが、こんな ViewController があったとします。
class ViewController: UIViewController {
init() {
super.init(nibName: nil, bundle: nil)
print("call 1")
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
print("call 2")
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
print("call 3")
}
}
イニシャライザを書くとこんな感じになります。
Storyboardから呼び出そうとするとクラッシュするように、required init?(coder: NSCoder) を挿入するようにXcodeから怒られるので、言われるがままに挿入します。
print文で書いている通り、init() -> viewDidLoad() -> viewDidAppear(_ animated: Bool) の順で呼ばれます。
ライフサイクルメソッドは他にもいろいろありますが、この記事では viewDidLoad() の話題に集中したいので、割愛します。
ここまでは前提です。
viewDidLoad() の正しい理解
僕はviewDidLoad()、UIViewController がメモリ上にロードされたときに呼ばれると思っていました。
公式ドキュメントをよく読むと、こう書いてあります。
This method is called after the view controller has loaded its view hierarchy into memory.
ViewControllerがメモリにロードされたとき、とは書いてありますが、よく読むと、「its view hierarchy」とも書いてあります。
つまり正しくは、
viewDidLoad()が呼ばれるのは、メモリ上のビュー階層にViewControllerがロードされたとき
というのが正しい理解となります。
思ったのと違うタイミングで呼ばれたとき
たいていのケースでは、この違いはそんなに問題になりません。
ViewControllerを呼んだら、だいたいすぐに pushViewController() するか、 present() するかして、画面遷移するからです。
ただ今回僕が経験したのは、親子関係をつけたViewControllerでした。
子側のViewControllerは2つあって、それを切り替える設計です。
コードを抜粋すると、こんな感じ。
// ViewControllerはlazy varで定義
// …
// ViewControllerへのDI
viewController.willMove(toParent: self)
view.addSubview(viewController.view)
addChild(viewController)
viewController.didMove(toParent: self)
このコードだと、どこで viewDidLoad() が呼ばれるでしょうか?
正解はview.addSubview(viewController.view) のところで呼ばれます。
viewにアクセスしたタイミングでロードが走る
addSubview() が条件なのか、というと、それも正確な理解ではありません。
正確には、ViewControllerの保持しているviewにアクセスしたときに、loadView() が呼ばれて、
その後でviewDidLoad() が呼ばれる、という流れらしいです。
問題になるとき
前述の通り、たいていのケースではここまで理解してなくても、問題になるときはないと思います。
今回僕が遭遇したケースの場合は、
lazy var childViewController1 = …
lazy var childViewController2 = …
private var presenter1 = …
private var presenter2 = …
func switchViewController() {
if isUseViewController1 {
childViewController1.inject(presenter1)
} else {
childViewController2.inject(presenter2)
}
// 子ViewControllerのadd
}
みたいなことをしていて、childViewController1、childViewController2の viewDidLoad() の中でAPIのコールをする、ということをやっていました。
こういう感じでViewControllerをイニシャルしてから、ビューにアクセスする間にいろいろ処理してると、
viewDidLoad() の呼ばれるタイミングが直感とズレます。
まとめ
-
viewDidLoad()が呼ばれるタイミングは、ViewControllerの保持しているviewにアクセスしたとき - なので、開発者の直感とズレることがあるので、気をつける
(了)
Discussion