Swiftで遊ぼう! - 294 - SwiftUIにおけるViewのライフサイクル(旧題:viewDidLoad、viewWillAppear、viewWillDisappearのタイミング)
- SwiftU(Swift5.1)I対応に向けて書き換え
2020年3月22日:SwiftUIで簡単になったViewのライスサイクルの説明をするので、以前書いた古い記事は残すけど、「Obsolete」ラベルをつけて差別化
2016年10月31日:Swift 3向けに変更
2016年6月3日:大幅に改訂*1
SwiftUIにおけるViewのライスサイクル
SwiftUIを使って開発をすれば、表示と変数の変化(状態変化)を完全に区別することができる。SwiftUIは使ってレイアウトを考えている時に、状態変化による変数の変化を意識せずデザイン設計作業に集中できます。状態変化によるレイアウト変化は、根本的にKVOの考え方に基づいたEnvironmentObjectに任せてしまえばいいわけです。ということでViewの表示のタイミングを考える必要もなく、UIKitで煩わしく思われた「viewDidLoat()」にメソッドを組み込むことなんて全然必要なし!忘れてしまってもいいんですよ。SwiftUIでViewの表示の挙動をみるのは、「onAppear」と「onDisappear」の2つのメソッドだけ。ちょっとプロジェクトを作って次のコードを組み込んでみるといい。
import SwiftUI struct ContentView: View { var body: some View { NavigationView { NavigationLink(destination: DetailView()) { Text("Go DetailView()") } } .onAppear { print("ContentView 表示された!") } .onDisappear { print("ContentView 消えた!") } } } struct DetailView: View { @State var size: CGFloat = 1.0 var body: some View { VStack { Text("DetailView") .font(.system(size: 300)) .scaleEffect(size) .animation(.easeOut(duration: 2)) .fixedSize() }.onAppear { print("DetailView 表示された!") withAnimation { self.size = 0.1 } }.onDisappear { print("DetailView 消えた!") } } }
このプロジェクトのシュミレーターで動かすと(プレビューでは表示されません)、画面が表示されると「ContentView 表示された!」が表示され、ボタンを押した瞬間に「"DetailView 表示された!」表示されてからアニメーションが動きます。このときContentViewは消えていないようです。「Back」ボタンを押せば、「DetailView 消えた!」が表示されて消滅しました。ContentViewを明示的に消す方法もあると思いますが、今は分かりません。
以下は古くさい情報なので、SwiftUIで開発時に必要ない情報です。
View Controller Lifecycle
スタンフォード大学のiOS開発講座を聴講してます。ViewControllerのライフサイクルを知っておくことが重要なので、過去のこの記事をリフレッシュして考察を加えます。古い記事も最後に引用文として残しますが、内容は不十分だったことが分かりました。
新しいデモプロジェクトとして「VCLifecycle」を作ります。Single View Applicationを選択して、storyboardに2つのViewControllerを並べます。FirstViewControllerとSecondViewControllerです。それぞれのViewに1つだけボタンを設置します。FirstViewControllerには「-> Second」ボタン、SecondViewControllerには「-> First」にします。そしてボタンからSegueで互いのViewControllerに「Show」で繋ぎます。
次にそれぞれのVIewControllerに次のコードを書きます。
// FirstViewControllerに加えるコード import UIKit var firstVCCount = 0 // 1stVCのインスタンス数をグローバル変数でカウント class FirstViewController: UIViewController { override func awakeFromNib() { super.awakeFromNib() print("1stViewController's awakeFromNib() is called") } override func viewDidLoad() { super.viewDidLoad() firstVCCount += 1 print("1stViewController's viewDidLoad() is called -----> (Count = \(firstVCCount))") } deinit { firstVCCount -= 1 print("1stViewController is cleared -----> (Count = \(firstVCCount))") } override func viewWillAppear(_ animated: Bool) { super.viewDidDisappear(animated) print("1stViewController's viewWillAppear() is called") } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) print("1stViewController's viewDidAppear() is called") } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) print("1stViewController's viewWillDisappear() is called") } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) print("1stViewController's viewDidDisappear is called") } override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() print("1stViewController's viewWillLayoutSubviews() is called") } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() print("1stViewController's viewDidLayoutSubviews() is called") } }
前の記事では不十分だったViewControllerがインスタンス化する時に呼ばれるメソッドをすべてコンソールに表示させてみました。
// SecondViewControllerも同じようにコードします。 import UIKit var secondVCCount = 0 class SecondViewController: UIViewController { override func awakeFromNib() { super.awakeFromNib() print("2ndViewController's awakeFromNib() is called") } override func viewDidLoad() { super.viewDidLoad() secondVCCount += 1 print("2ndViewController's viewDidLoad() is called -----> (Count = \(secondVCCount))") } deinit { secondVCCount -= 1 print("2ndViewController is cleared -----> (Count = \(secondVCCount))") } override func viewWillAppear(_ animated: Bool) { super.viewDidDisappear(animated) print("2ndViewController's viewWillAppear() is called") } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) print("2ndViewController's viewDidAppear() is called") } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) print("2ndViewController's viewWillDisappear() is called") } override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) print("2ndViewController's viewDidDisappear is called") } override func viewWillLayoutSubviews() { super.viewWillLayoutSubviews() print("2ndViewController's viewWillLayoutSubviews() is called") } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() print("2ndViewController's viewDidLayoutSubviews() is called") } }
このプロジェクトをランさせるとまず次のようにコンソールに表示されます。
// Xcode 7までのコンソール表示 1stViewController's awakeFromNib() is called 1stViewController's viewDidLoad()is called -----> (Count = 1) 1stViewController's viewWillAppear() is called 1stViewController's viewWillLayoutSubviews() is called 1stViewController's viewDidLayoutSubviews() is called 1stViewController's viewWillLayoutSubviews() is called 1stViewController's viewDidLayoutSubviews() is called 1stViewController's viewDidAppear() is called // Xcode 8からコンソール表示が複雑になりました。前半部分は省略してます。 1stViewController's awakeFromNib() is called 1stViewController's viewDidLoad()is called -----> (Count = 1) 1stViewController's viewWillAppear() is called 1stViewController's viewWillLayoutSubviews() is called 1stViewController's viewDidLayoutSubviews() is called 2016-10-31 23:31:13.857328 VLCLifecycle[1835:1057668] subsystem: com.apple.BackBoardServices.fence, category: App, enable_level: 1, persist_level: 0, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0, enable_oversize: 0, privacy_setting: 0, enable_private_data: 0 1stViewController's viewDidAppear() is called
LayoutSubviewsは複数呼ばれています。これはViewがジオメトリックに変化した時に必ず呼ばれるようです。viewDidLoadはインスタンスが作られた時に1回しかよばれません。そこでインスタンス作成回数を1増やします。このインスタンスがメモリーヒープから消去される時に呼ばれるメソッドが「deinit」なので、その中でカウントを減らしています。当然アプリが立ち上がった時はFirstViewControllerしか作られません。
次に「-> Seconf」ボタンを押したら次のようにメッセージが流れます。
// Xcode 7までのコンソール表示 2ndViewController's awakeFromNib() is called 2ndViewController's viewDidLoad()is called -----> (Count = 1) 1stViewController's viewWillDisappear() is called 2ndViewController's viewWillAppear() is called 2ndViewController's viewWillLayoutSubviews() is called 2ndViewController's viewDidLayoutSubviews() is called 1stViewController's viewWillLayoutSubviews() is called 1stViewController's viewDidLayoutSubviews() is called 2ndViewController's viewWillLayoutSubviews() is called 2ndViewController's viewDidLayoutSubviews() is called 2ndViewController's viewDidAppear() is called 1stViewController's viewDidDisappear is called // Xcode 8からコンソール表示です。 2ndViewController's awakeFromNib() is called 2ndViewController's viewDidLoad()is called -----> (Count = 1) 1stViewController's viewWillDisappear() is called 2ndViewController's viewWillAppear() is called 2ndViewController's viewWillLayoutSubviews() is called 2ndViewController's viewDidLayoutSubviews() is called 1stViewController's viewWillLayoutSubviews() is called 1stViewController's viewDidLayoutSubviews() is called 2ndViewController's viewDidAppear() is called 1stViewController's viewDidDisappear is called
Segueは新しいViewControllerを作ってFirstViewControllerの上に表示するだけです。次に「-> First」ボタンを押したら次のようになります。
// Xcode 7までのコンソール表示 1stViewController's awakeFromNib() is called 1stViewController's viewDidLoad()is called -----> (Count = 2) 2ndViewController's viewWillDisappear() is called 1stViewController's viewWillAppear() is called 1stViewController's viewWillLayoutSubviews() is called 1stViewController's viewDidLayoutSubviews() is called 2ndViewController's viewWillLayoutSubviews() is called 2ndViewController's viewDidLayoutSubviews() is called 1stViewController's viewWillLayoutSubviews() is called 1stViewController's viewDidLayoutSubviews() is called 1stViewController's viewDidAppear() is called 2ndViewController's viewDidDisappear is called // Xcode 8からコンソール表示です。 1stViewController's awakeFromNib() is called 1stViewController's viewDidLoad()is called -----> (Count = 2) 2ndViewController's viewWillDisappear() is called 1stViewController's viewWillAppear() is called 1stViewController's viewWillLayoutSubviews() is called 1stViewController's viewDidLayoutSubviews() is called 1stViewController's viewDidAppear() is called 2ndViewController's viewDidDisappear is called
あれれ、FirstViewControllerのインスタンスが再び作られました。そうなんですSegueは新しいViewControllerを作って重ねていくだけなんで、元のFIrstVIewControllerの上にSecondViewControllerに作られ、その上に再びFirstViewControllerが作られているだけす。もう一度「-> Second」ボタンを押したら...
// Xcode 7までのコンソール表示 2ndViewController's awakeFromNib() is called 2ndViewController's viewDidLoad()is called -----> (Count = 2) 1stViewController's viewWillDisappear() is called 2ndViewController's viewWillAppear() is called 2ndViewController's viewWillLayoutSubviews() is called 2ndViewController's viewDidLayoutSubviews() is called 1stViewController's viewWillLayoutSubviews() is called 1stViewController's viewDidLayoutSubviews() is called 2ndViewController's viewWillLayoutSubviews() is called 2ndViewController's viewDidLayoutSubviews() is called 2ndViewController's viewDidAppear() is called 1stViewController's viewDidDisappear is called // Xcode 8からコンソール表示です。 2ndViewController's awakeFromNib() is called 2ndViewController's viewDidLoad()is called -----> (Count = 2) 1stViewController's viewWillDisappear() is called 2ndViewController's viewWillAppear() is called 2ndViewController's viewWillLayoutSubviews() is called 2ndViewController's viewDidLayoutSubviews() is called
もう説明はいりませんね。ボタンを押す度にViewControllerが作られ増えていきます。こういうアプリを作ったら大変です。これがメモリーリークですね。
じゃあどうすべきか? 実は「-> First」ボタンで元のFirstViewControllerに戻ってやればいいんです。ここでunwind segueを使います。FirstViewControllerに次のコードを加えます。
@IBAction func unwindToTop(segue: UIStoryboardSegue) { }
そして、「-> First」ボタンに作ったSegueを消して、「Ctrl + ドラッグ」で「Exit」ボタンに持っていくと、FirstViewControllerに加えたメソッド「unwindToTop」が出てくるの選択します。
これでメモリーリークが解消されます。同じようにランさせます。
1stViewController's awakeFromNib() is called 1stViewController's viewDidLoad()is called -----> (Count = 1) 1stViewController's viewWillAppear() is called 1stViewController's viewWillLayoutSubviews() is called 1stViewController's viewDidLayoutSubviews() is called 1stViewController's viewWillLayoutSubviews() is called 1stViewController's viewDidLayoutSubviews() is called 1stViewController's viewDidAppear() is called
立ち上がった時は同じです。そして「ー> Second」ボタンを押した時も同様です。
2ndViewController's awakeFromNib() is called 2ndViewController's viewDidLoad()is called -----> (Count = 1) 1stViewController's viewWillDisappear() is called 2ndViewController's viewWillAppear() is called 2ndViewController's viewWillLayoutSubviews() is called 2ndViewController's viewDidLayoutSubviews() is called 1stViewController's viewWillLayoutSubviews() is called 1stViewController's viewDidLayoutSubviews() is called 2ndViewController's viewWillLayoutSubviews() is called 2ndViewController's viewDidLayoutSubviews() is called 2ndViewController's viewDidAppear() is called 1stViewController's viewDidDisappear is called
FirstViewCOntrollerの上にSecondViewControllerができるのは同じです。しかし次に「-> First」ボタンを押した時にunwind segueが生じます。
2ndViewController's viewWillDisappear() is called 1stViewController's viewWillAppear() is called 2ndViewController's viewWillLayoutSubviews() is called 2ndViewController's viewDidLayoutSubviews() is called 1stViewController's viewWillLayoutSubviews() is called 1stViewController's viewDidLayoutSubviews() is called 1stViewController's viewDidAppear() is called 2ndViewController's viewDidDisappear is called 2ndViewController is cleared -----> (Count = 0) 1stViewController's viewWillLayoutSubviews() is called 1stViewController's viewDidLayoutSubviews() is called
SecondViewControllerが消されて、下にあったFirstViewControllerが再表示されました。ViewControllerのライフサイクルを理解するって本当に重要ですね。
2015年6月30日:Xcode 7.0 beta(7A120f)を使っても同様でした。しかし実行時にエラーが出るので修正します(println→print)
UIImageとUIImageViewの実装説明をしていたのですが、このデモの中でviewWillAppear()というメソッドが出てきたので、どういうタイミングでこのメソッドが呼ばれるのか確かめることにしました。(Xcode6.3.2およびXcode7.0beta(7A120f) )
元ネタは次のブログ記事です。
blog.livedoor.jpこの記事によると、それぞれ呼ばれるタイミングは次のようになっています。
- viewDidLoad:インスタンス化された直後に一度だけ
- viewWillAppear:画面が表示される直前
- viewDidAppear:画面が表示された直後
- viewWillDisappear:画面が消える直前
- viewDidDisappear:画面が消えた直後
じゃあ新しいプロジェクトを作って確かめてみます。
3つのUIViewControllerを並べて、左端から「HomeViewController」「FirstViewController」「SecondViewController」と名前をつけ、それぞれのViewの中央にボタンを設置し。それぞれのボタンに、「First 」「Second」「Home」と名前を変更して、Segue(セグエ)で繋いでいきます。このセグエは「Show」を選びました。
まずHomeViewControllerのコーディングを次のようにします。
class HomeViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() print("HomeViewControllerのviewDidLoadが呼ばれた") } override func viewWillAppear(animated: Bool) { super.viewDidDisappear(animated) print("HomeViewControllerのviewWillAppearが呼ばれた") } override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) print("HomeViewControllerのviewDidAppearが呼ばれた") } override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) print("HomeViewControllerのviewWillDisappearが呼ばれた") } override func viewDidDisappear(animated: Bool) { super.viewDidDisappear(animated) print("HomeViewControllerのviewDidDisappearが呼ばれた") } }次にFirstViewControllerのコーディングを次のようにします。
class FirsViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() print("FirstViewControllerのviewDidLoadが呼ばれた") } override func viewWillAppear(animated: Bool) { super.viewDidDisappear(animated) print("FirstViewControllerのviewWillAppearが呼ばれた") } override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) print("FirstViewControllerのviewDidAppearが呼ばれた") } override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) print("FirstViewControllerのviewWillDisappearが呼ばれた") } override func viewDidDisappear(animated: Bool) { super.viewDidDisappear(animated) print("FirstViewControllerのviewDidDisappearが呼ばれた") } }最後にSecondViewControllerのコーディングを次のようにします。
class SecondViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() print("SecondViewControllerのviewDidLoadが呼ばれた") } override func viewWillAppear(animated: Bool) { super.viewDidDisappear(animated) print("SecondViewControllerのviewWillAppearが呼ばれた") } override func viewDidAppear(animated: Bool) { super.viewDidAppear(animated) print("SecondViewControllerのviewDidAppearが呼ばれた") } override func viewWillDisappear(animated: Bool) { super.viewWillDisappear(animated) print("SecondViewControllerのviewWillDisappearが呼ばれた") } override func viewDidDisappear(animated: Bool) { super.viewDidDisappear(animated) print("SecondViewControllerのviewDidDisappearが呼ばれた") } }じゃあラン(Cmd + R)します。アプリが立ち上がると次のメッセージがコンソールに現れます。
HomeViewControllerのviewDidLoadが呼ばれた HomeViewControllerのviewWillAppearが呼ばれた HomeViewControllerのviewDidAppearが呼ばれた次に画面中央の「First」ボタンを押すと次のメッセージがコンソールに現れます。
FirstViewControllerのviewDidLoadが呼ばれた HomeViewControllerのviewWillDisappearが呼ばれた FirstViewControllerのviewWillAppearが呼ばれた FirstViewControllerのviewDidAppearが呼ばれた HomeViewControllerのviewDidDisappearが呼ばれた画面が切り替わり「Second」ボタンを押すと次のようになります。
SecondViewControllerのviewDidLoadが呼ばれた FirstViewControllerのviewWillDisappearが呼ばれた SecondViewControllerのviewWillAppearが呼ばれた SecondViewControllerのviewDidAppearが呼ばれた FirstViewControllerのviewDidDisappearが呼ばれたそして最後に「Home」ボタンを押します。
HomeViewControllerのviewDidLoadが呼ばれた SecondViewControllerのviewWillDisappearが呼ばれた HomeViewControllerのviewWillAppearが呼ばれた HomeViewControllerのviewDidAppearが呼ばれた SecondViewControllerのviewDidDisappearが呼ばれた元ネタのブログ記事と挙動がかなり異なります。元ネタの記事がアップされた時期が2014年3月なので、Xcode6はまだ発表されていないので新しくなって変更が加えられてようですね。というのもセグエの扱いにかなり変更が加えられたからです。
デモのSegue(セグエ)ですが、「Show」から「Show Detail」に変更しても呼ばれ方は変化しませんでした。
*1:オリジナルも最後に残しておきます。