Swiftで遊ぼう! on Hatena

あしたさぬきblogでやってた初心者オヤジのiOSプログラミング奮闘記がHatenaに来ました

Swiftで遊ぼう! - 294 - viewDidLoad、viewWillAppear、viewWillDisappearのタイミング

2016年10月31日:Swift 3向けに変更
2016年6月3日:大幅に改訂*1

View Controller Lifecycle

スタンフォード大学iOS開発講座を聴講してます。ViewControllerのライフサイクルを知っておくことが重要なので、過去のこの記事をリフレッシュして考察を加えます。古い記事も最後に引用文として残しますが、内容は不十分だったことが分かりました。

yataiblue.hatenablog.com

新しいデモプロジェクトとして「VCLifecycle」を作ります。Single View Applicationを選択して、storyboardに2つのViewControllerを並べます。FirstViewControllerSecondViewControllerです。それぞれのViewに1つだけボタンを設置します。FirstViewControllerには「-> Second」ボタン、SecondViewControllerには「-> First」にします。そしてボタンからSegueで互いのViewControllerに「Show」で繋ぎます。

f:id:yataiblue:20161031233542j:plain

次にそれぞれの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」が出てくるの選択します。

f:id:yataiblue:20160604153116j:plain

これでメモリーリークが解消されます。同じようにランさせます。

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

この記事によると、それぞれ呼ばれるタイミングは次のようになっています。

  1. viewDidLoadインスタンス化された直後に一度だけ
  2. viewWillAppear:画面が表示される直前
  3. viewDidAppear:画面が表示された直後
  4. viewWillDisappear:画面が消える直前
  5. viewDidDisappear:画面が消えた直後

じゃあ新しいプロジェクトを作って確かめてみます。

3つのUIViewControllerを並べて、左端から「HomeViewController」「FirstViewController」「SecondViewController」と名前をつけ、それぞれのViewの中央にボタンを設置し。それぞれのボタンに、「First 」「Second」「Home」と名前を変更して、Segue(セグエ)で繋いでいきます。このセグエは「Show」を選びました。

f:id:yataiblue:20150428140848j:plain


まず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:オリジナルも最後に残しておきます。