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