iOS13以降でviewWillAppearやviewDidAppearが呼ばれない場合の対処法

4 min read読了の目安(約3800字

例えば ViewController AからViewController Bを表示してデータの値を書き換える。そしてViewController Bでの作業が終わって閉じたらViewController AのviewWillAppearviewDidAppearで新しいデータの値を表示する(tableview.reloadData()など)のは良くあるパターン。

今更な話なのだがiOS12までは動いていたこのコードがそのままだとiOS13以降では動かなくなることがある。これで何度かつまずいたので簡単にまとめておく。

結論から

まずは下記を確認。

https://developer.apple.com/videos/play/wwdc2019/224/
そしてこれをダウンロードしてよく読む。
https://developer.apple.com/documentation/uikit/view_controllers/disabling_pulling_down_a_sheet
最後にUIAdaptivePresentationControllerDelegateを使って今までのコードを書き換える。

これで何故動かなくなったのか?どうすれば良いのか?がわかるはず。

以下、いくつかの解決方法について簡単に説明。

フルスクリーン表示にしてしまう

1つ目はViewController Bをフルスクリーンにしてしまう方法。これで今まで通りにViewController AのviewWill/DidAppearが呼ばれる。
iOS13以降は今までのように何も考えずに新しい画面を表示するとシートタイプの画面が表示されてviewWill/DidAppearが呼ばれないので、これを全画面表示にしてiOS12以前の挙動と同じにするようにする。

StoryboardならSegueの設定でPresent ModallyFull Screenを設定。

コードの場合はSecondViewControllerを開くとして以下のようになる。

secondViewController.modalPresentationStyle = .fullScreen
present(secondViewController, animated: true, completion: nil)

今までのコードがうまく動かなくなったと言う事は、元々新しい画面はフルスクリーンで表示されていたので違和感はないはず。
だが、すでにiOS14が出ている現在ではユーザには今時の挙動と少々違って感じるかもしれない。

スワイプで閉じられなくする

isModalInPresentationと言うプロパティがあるので、ViewController Bでこれをfalseにしてスワイプで画面を閉じられなくする。必然的にunwinddismissなどコードやSegueで画面を閉じてViewController Aに戻らなければならないので、そこで閉じるイベントをViewController Aに伝えるかデータを再読み込みするコードを書いておく。

が、見た目は今時のシートなのにスワイプで閉じられないのはユーザに違和感を与える可能性が高い。

UIAdaptivePresentationControllerDelegateを使う

前出のビデオとサンプルコードでやっている方法。
シートをスワイプで閉じて良いかどうかなどの確認をUIAdaptivePresentationControllerDelegateを通じて行うので、これをViewController Bで受け取ってViewController Bが閉じるタイミングを検知。それをプロトコルでViewController Aへ伝える。

具体的にはこんな感じ。
とりあえずUIAdaptivePresentationControllerDelegateの部分。
各種イベントを受け取ってsheetが閉じた時にdelegateになっている元のViewController AのviewDidDismissを呼び出している。ここでViewController Aがデータのアップデートを行う。
詳細はサンプルコードを確認のこと。

extension ThirdViewController: UIAdaptivePresentationControllerDelegate {
    func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
        print("\(type(of: self)): \(#function)")
        return true
    }
    
    func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
        print("\(type(of: self)): \(#function)")
    }
    
    func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
        print("\(type(of: self)): \(#function)")
    }
    
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        print("\(type(of: self)): \(#function)")
        delegate?.viewDidDismiss()
    }
}

どれが良いのか?

新しいアプリならUIAdaptivePresentationControllerDelegateを使うのが今風で良いと思う。が、既存のアプリの場合はフルスクリーンにするかUIAdaptivePresentationControllerDelegateかのどちらかをUXとの兼ね合いで決めると良いだろう。

コード

フルスクリーンにする方法とUIAdaptivePresentationControllerDelegateを使う方法を試したサンプルをGitHubにあげておいた。
それぞれの方法でどの順番で何が呼ばれるかを表示している。

https://github.com/paraches/sheetStudy
ちなみにログは下記のようになる。
// 起動時Viewが表示される
ViewController: viewWillAppear(_:)
ViewController: viewDidAppear(_:)
// Full Screen クリックてSecondViewが表示される
SecondViewController: viewWillAppear(_:)
SecondViewController: viewDidAppear(_:)
// backをクリックして元のViewが表示される
ViewController: unwindToFirstViewController(_:)
ViewController: viewWillAppear(_:)
ViewController: viewDidAppear(_:)

// sheetをクリックしてThirdViewが表示される
ThirdViewController: viewWillAppear(_:)
ThirdViewController: viewDidAppear(_:)
// sheetをドラッグして閉じViewに戻る
ThirdViewController: presentationControllerShouldDismiss(_:)
ThirdViewController: presentationControllerWillDismiss(_:)
ThirdViewController: presentationControllerDidDismiss(_:)
ViewController: viewDidDismiss()

最後に

いまだに「あれ?viewWillAppearが呼ばれない?」なんてやっているので、簡単に思い出せるようにメモ。
これに限らずこう言うことが増えているのでマメにメモしていかないといけない。