Developer's Blog

viewMightAppear?


navigation_edge_swipe

こんにちは。アプリケーション共同開発部 開発担当の藤重です。

iOS の 主要 UI クラスである、 UIViewController には、 ViewController が画面に表示される時・非表示になる時に呼ばれる以下のメソッドがあります。

  • viewWillAppear : 表示される直前
  • viewDidAppear : 表示完了時
  • viewWillDisappear : 非表示になる直前
  • viewDidDisappear : 非表示完了時

viewWillAppear / viewDidAppear、 viewWillDisappear / viewDidDisappear はそれぞれ対で呼ばれることを期待しますが、 iOS 7 以後では、 will だけ呼ばれて、対になる did が呼ばれないケースがあります。

did が呼ばれないのはどのような場合か、また、呼ばれない did 相当のタイミングを検知するにはどうすれば良いかについて解説します。

viewDidAppear・viewDidDisappear が呼ばれないケース

iOS 7 で、 UIViewController の画面遷移(Modal 表示や UINavigationController の Push・Pop など)のアニメーションをカスタマイズする機能が追加されました。カスタマイズされた画面遷移は、『Custom Transition』と呼ばれています。

画面遷移は、遷移アニメーションにユーザが介入できないものと、ユーザが途中経過を操作できるものの二種類が存在します。標準 UI では、モーダル表示が前者、 UINavigationController の Pop が後者の挙動となっています。ユーザが途中経過を操作できるものを、『Interactive Transition』と呼びます。

Interactive Transition が途中でキャンセルされた場合、 viewWillAppear / viewWillDisappear だけが呼ばれ、 対になる viewDidAppear / viewDidDisappear は呼ばれません。

UINavigationController を利用している場合、画面左端からのスワイプ操作で Interactive Transition による Pop 画面遷移が開始します。 この操作は、完全に Pop して前画面に戻るとは限らず、前の画面が少し表示された状態で左へのスワイプを行うことでキャンセルが可能です。 この時、前述の通り viewDidAppear / viewDidDisappear は呼ばれません。

uinavigationcontroller_interactive_pop

具体的には、以下のような挙動になります。

  • 遷移元 ViewController の viewWillDisappear は呼ばれるが、viewDidDisappear が呼ばれない。
  • 遷移先 ViewController の viewWillAppear は呼ばれるが、viewDidAppear が呼ばれない。

表示関連メソッドの呼び出し詳細

viewWillAppear / viewWillDisappear、 viewDidAppear / viewDidDisappear が呼び出される順序について詳しく見ていきます。

UINavigationController で、 ViewController1 → ViewController2 と Push 遷移している状況を想定します。 (以下、ViewController1・ViewController2 は VC1・VC2 と表記)

通常ケース

通常の Pop 遷移を行った場合、表示関連メソッドは以下のように呼び出されます。

  • VC2 : viewWillDisappear
  • VC1 : viewWillAppear
  • ※ Pop アニメーション
  • VC2 : viewDidDisappear
  • VC1 : viewDidAppear

画面端からのジェスチャを行った場合も、キャンセルされずに遷移が完了した場合は同様の流れになります。

Interactive Transition が途中でキャンセルされたケース

画面端からスワイプを開始し、途中で元に戻して Pop 遷移をキャンセルした場合、表示関連メソッドは以下のように呼び出されます。

  • ※ スワイプジェスチャ開始
  • VC2 : viewWillDisappear
  • VC1 : viewWillAppear
  • ※ 元の位置にスワイプし直して Pop 遷移をキャンセル
  • ※ VC2 : viewDidDisappear は呼ばれない
  • ※ VC1 : viewDidAppear は呼ばれない
  • ※ 各 ViewController は再表示扱いになり、以下のメソッドが呼ばれる
  • VC1 : viewWillDisappear
  • VC1 : viewDidDisappear
  • VC2 : viewWillAppear
  • VC2 : viewDidAppear

通常ケースで呼ばれる、VC2 : viewDidDisappear・VC1 : viewDidAppear が呼ばれていないことがわかります。 また、キャンセル後、各 ViewController で表示関連メソッドが再度呼び出されることに注意してください。

Interactive Transition キャンセルの検知

Interactive Transition がキャンセルされた際の対応について見ていきましょう。

通常の画面遷移か、Interactive Transition かに関わらず、 画面遷移中は UIViewController.transitionCoordinator から、 画面遷移に関する情報を持つ、 UIViewControllerTransitionCoordinator を取り出せます。

UIViewControllerTransitionCoordinator の以下の機能を利用して、 Interactive Transition のキャンセルタイミングを検知します。

  • 遷移が Interactive Transition として開始したかを判定する、”initiallyInteractive”
  • Interactive Transition の完了通知ブロックを登録する、”notifyWhenInteractionEndsUsingBlock”
  • Interactive Transition がキャンセルされたかどうかを判定する、”UIViewControllerTransitionCoordinatorContext.isCancelled()”

遷移が Interactive Transition の場合は、完了通知ブロックを登録して待ち受けます。

// viewWillDisappear も同じ
override func viewWillAppear(animated: Bool) {
  super.viewWillAppear(animated)

  if let coordinator = self.transitionCoordinator() {

    if coordinator.initiallyInteractive() {

      coordinator.notifyWhenInteractionEndsUsingBlock {

        context in
        // context は、
        // UIViewControllerTransitionCoordinatorContext

        if context.isCancelled() {
          // キャンセルされた場合は、
          // ここが viewDidAppear と同じタイミングとなる。
        }
      }
    }
  }
}

登録した完了通知ブロック内で、UIViewControllerTransitionCoordinatorContext.isCancelled() を利用して、Interactive Transition がキャンセルされたかどうかを判定することができます。Interactive Transition がキャンセルされた場合は、 viewDidAppear / viewDidDisappear の代わりに、完了通知ブロックが “context.isCancelled() == true” で呼び出されます。

VC2 : viewWillDisappear・VC1 : viewWillAppear で完了通知ブロックを登録した場合、キャンセル時の流れは次のようになります。

  • ※ スワイプジェスチャ開始
  • VC2 : viewWillDisappear
  • VC1 : viewWillAppear
  • ※ 元の位置にスワイプし直して Pop 遷移をキャンセル
  • VC2 : viewWillDisappear で登録した完了通知ブロックが、 “context.isCancelled() == true” で呼ばれる
  • VC1 : viewWillAppear で登録した完了通知ブロックが、 “context.isCancelled() == true” で呼ばれる。
  • ※ 各 ViewController は再表示扱いになり、以下のメソッドが呼ばれる
  • VC1 : viewWillDisappear
  • VC1 : viewDidDisappear
  • VC2 : viewWillAppear
  • VC2 : viewDidAppear

Interactive Transition キャンセルで、ViewController の表示メソッドが再度呼び出される

Interactive Transition がキャンセルされた場合、最後に ViewController の表示関連メソッドが再度呼び出されます。

  • VC1 : viewWillDisappear
  • VC1 : viewDidDisappear
  • VC2 : viewWillAppear
  • VC2 : viewDidAppear

VC1 → VC2 と Push 遷移した時と同じ流れですね。

ここで注意したいのは、VC2 の viewWillAppear / viewDidAppear が ViewController のライフサイクル上、 複数回呼ばれることを考慮する必要があるという点です。

ViewController からモーダルで別の ViewController を表示する流れがある場合、 モーダルを表示した側の ViewController では表示関連メソッドが複数回呼ばれますが、 Interactive Transition のキャンセルでもモーダルの場合と同じ考慮が必要ということになります。

参考リンク

本記事の内容は、 Custom Transitions Using View Controllers – WWDC 2013 / Session 218 で紹介されています。(ビデオの 43:30 あたり)

“Some colleagues of mine have rid me kind of ruthlessly that view will appear really should probably be called view might appear, or view will probably appear, or I really wish this view would appear.”

viewWillAppear = 『viewが表示されます』とう名前なのに viewDidAppear が呼ばれないケースが出てきたことに対して、 viewMightAppear = 『viewが表示されるかもしれません』などに変えようか?という話があったというジョークです。

結果としては viewWillAppear のままとなっているのは賢明な決定ですね。

備考

UIViewControllerTransitionCoordinator.notifyWhenInteractionEndsUsingBlock は iOS 10 で deprecated になっており、 代わりに UIViewControllerTransitionCoordinator.notifyWhenInteractionChangesUsingBlock を使うよう推奨されています。

まとめ

  • Interactive Transition がキャンセルされると、viewDidAppear / viewDidDisappear が呼ばれない。
  • 呼ばれなくなったメソッド相当のタイミングは、 UIViewController.transitionCoordinator に完了通知を登録して検知する。
  • Interactive Transition がキャンセルされると、 関連する ViewController の表示関連メソッドが再度呼ばれる。

iOS 7 以後は、Interactive Transition のキャンセルに関連して、 ViewController の表示関連メソッドの呼び出され方のパターンが増えたということを念頭に置く必要があります。

フェンリルのオフィシャル Twitter アカウントでは、フェンリルプロダクトの最新情報などをつぶやいています。よろしければフォローしてください!

Copyright © 2019 Fenrir Inc. All rights reserved.