Swiftで遊ぼう! on Hatena

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

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のライフサイクルを知っておくことが重要なので、過去のこの記事をリフレッシュして考察を加えます。古い記事も最後に引用文として残しますが、内容は不十分だったことが分かりました。

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