だからアプリを作る場合、前の画面に戻るためのUIは必要に応じて自分で実装しないといけないんですが、一応、自動的に画面の履歴を管理して、1つ前の画面に戻るボタンも表示してくれる、UINavigationController って機能が用意されてます。
それを使えば Android と同じような感じのアプリを作りやすいのですが、なんかどうにも微妙に不親切なので、罠っぽいところをまとめときます。
それはそうと、UINavigationController っていちいち書くのダルイよね。iOS の開発してるとどうしても 「〜Controller」 って名前が多くなるんで、いちいち名前が長くなって鬱陶しい。
UINavigationController の使い方
基本的な使い方については、他にいくらでも親切な記事がありますので他の方にお任せします。Xcode の右下にも Navigation Controller ってパーツがあるのですが、アレは余計なオマケ付きで使いにくすぎるので、メニューから Editor > Emned in で追加するのがお勧めです。 初めて UINavigationController の使い方を見た時は、てっきり 「これは履歴管理機能をセットアップためだけに通り抜ける隠し画面なのかね?」 とか勝手に誤解して勝手に気持ち悪がったもんですが、UINavigationController が親で、表示する画面群が子、という親子関係だったんですね。各画面の
parent
プロパティは、1つ前の画面ではなく UINavigationController を指してます。
ちなみに、storyboard を複数に分けて開発してる場合も、以下みたいにすれば同じ UINavigationController で管理できます。この場合、新たにロードする storyboard の方には UINavigationController を繋ぐ必要は必要ありません。
swift3
if let vc = UIStoryboard( name: "storyboard名", bundle: nil ) .instantiateInitialViewController() { show( vc, sender: nil ) // もしくは以下でも可。show() は状況を判断して以下を呼んでる。 // navigationController?.pushViewController( vc, animated: true ) }
1つ前の画面を取得したいんだけど?
簡単に取得できないんだなこれが
ドン引きだよ何この役立たず!。1つ前の画面を取得したい場合、UINavigationController の画面スタック、viewControllers
の中から、自分の画面の1つ前の画面を探してこないといけません。以下みたいに extension にしちゃえば、どこでも流用できて便利です。
swift3
public extension UIViewController { public func getPreviousViewController() -> UIViewController? { if let vcList = navigationController?.viewControllers { var prevVc: UIViewController?; for vc in vcList { if ( vc == self ) { break } prevVc = vc } return prevVc } // ここに来るのは実装ミスなので assert でもよろし return nil } }
ちなみに、ネットで探すと 「viewControllers の末尾から2番目を取得すればOK!」 みたいな記事をよく見かけるのですが (つーか 公式ドキュメントにまでそう書いてるらしいorz)、この方法だとタイミングによっては2つ前の画面を取得しちゃうのでやめましょう。どうにも画面スタックに登録や削除されるタイミングが自分勝手なんですよ。
- 画面表示時ライフサイクル
viewDidLoad
> 接続前なので navigationController が nil (storyboard だと登録済の事もある)willMove(toParentViewController:parent)
> ここ以後はスタックに登録済viewWillAppear
> 登録済viewDidAppear
> 登録済
- Back 押下時ライフサイクル
willMove(toParentViewController:nil)
> ここまでは登録済viewWillDisappear
> スタックから削除されてるviewDidDisappear
> navigationController が nil
viewDidLoad
や、リザルトを返したい viewWillDisappear
が罠になってるのがタチ悪いですね。安全にアクセスしたいのなら、viewWillAppear
とか、スタックが安定してるタイミングで取得して、メンバに保持しておくのが安全かと。それと
viewDidLoad
については、呼び出されるタイミングがブラックボックス過ぎなので、自分や子の初期化だけにして、親や前の画面へのアクセスは避けた方がよさそう。
Back押した時に何か処理したいんだけど
簡単にできないんだなこれが
ドン引きだよ何この役立たず!。なんでか、Back を押された事を直接知る手立ては本気でないようですので、viewWillDisappear
や viewDidDisappear
で処理する事になります。が、これらは別の画面への遷移時などにも呼ばれるので、前の画面に戻り中なのか判別しないといけません。で、これまたネットで探すと、「
viewWillDisappear
内で、画面スタックに自分がないなら戻り中です!」 という、バッドノウハウな予感しかしない記事がいっぱい見つかるのですが、現在の挙動がたまたまそうってだけだし、どちらかというとまだ表示中なのに履歴から消されてる事の方が不自然なので、そんな方法で判断したくありません。正解は、戻る際には UIViewController の
isMovingFromParentViewController
というプロパティが true になるので、大抵の場合はこれで判断するのが確実なのですが、最初見た時 MovingFrom の意味が掴めなくて困った。RemoveFrom とか にしてくれよ。そしてさらにウザい事に、UINavigationController などに管理されてる画面とそうでない画面で、判断の仕方が違うんですよね。
isMovingToParentViewController
- 親 ViewController に接続されて初めて表示されるまでの一連の処理中は true
isMovingFromParentViewController
- 親 ViewController から切り離されて画面を閉じるまでの一連の処理中は true
isBeingPresented
-
presentViewController
で画面を開く際の一連の処理中は true isBeingDismissed
-
dismiss
で画面を閉じる際の一連の処理中は true
swift3
public extension UIViewController { public final var isPresenting : Bool { return isBeingPresented || isMovingToParentViewController } public final var isDismissing : Bool { return isBeingDismissed || isMovingFromParentViewController } }
画面スワイプでも戻れるんだ。そう、iPhoneならね!
上記で前の画面に戻る際の処理も記述できるようになりましたが、実は UINavigationController 管理下の画面は、Back ボタンのほかに、画面左端からスワイプでも前の画面に戻れます。 まあ分かりやすいし、いいんじゃないすかねとは思うのですが、困った事にこのスワイプ、途中でやめて左に戻せてしまいます。この時、ライフサイクル的には何が起きてるかというと、- Backボタン、または正常にスワイプしきった場合
-
<スワイプ開始時>
- 現在の画面の
willMove:toParentViewController:
- 現在の画面の
viewWillDisppear
(isMovingFromParentViewController = true) - 前の画面の
viewWillAppear
- 現在の画面の
viewDidDisppear
(isMovingFromParentViewController = true) - 現在の画面の
didMove:toParentViewController:
- 前の画面の
viewDidAppear
- 現在の画面の
- スワイプを途中でやめた場合
-
<スワイプ開始時>
- 現在の画面の
willMove:toParentViewController:
- 現在の画面の
viewWillDisppear
(isMovingFromParentViewController = true) - 前の画面の
viewWillAppear
- 前の画面の
viewWillDisappear
- 前の画面の
viewDidDisappear
- 現在の画面の
viewWillAppear
(isMovingToParentViewController = true) - 現在の画面の
viewDidAppear
(isMovingToParentViewController = true)
- 現在の画面の
な ん か い ろ い ろ と 乱 暴 ス ギ ィ!
さあーぁ、この挙動を把握してないといろんな事が起きますよ!
viewWillDisAppear
で前の画面にリザルトを返す場合、それが何度も呼ばれても問題ない作りにしないといけません。
viewDidDisappear
でなら本当の終了時にだけリザルトを返せますが、前の画面に完全に戻ってから画面に反映させる事になるので、見栄えはよくないです。viewDidDisappear
に終了処理などを書く場合、そこを通らずにviewWillAppear
から再初期化しても問題ない作りにしないといけません。
それに限らず、viewWill〜 と viewDid〜、willMove〜 と didMove〜 が必ず対で呼ばれる前提の処理は一切組めません。viewWillAppear
で状態の初期化などを行っていると、スワイプ中断時にも初期化されてしまう事になります。ユーザーとしては続きから操作をするつもりが最初からやり直しでイラっときますので注意が必要です。
まとめの愚痴
とまあ、いろいろ不親切とはいえ、自動的に画面履歴と Back ボタンを管理してくれるのは便利なので、今後も UINavigationController にはお世話になるとは思うのですけど、今日のどの件に関しても、というかiOS の相手しててちょくちょく思うのだけど、どうにも詰めが甘いままデベロッパに丸投げになってる仕様が多くて、ちょくちょく頭を抱えます。ボクの考えた素晴らしいアイディアを次々作るのもいいけど、ちゃんと詰めてほしいもんですね。しょーもないバグも多いし。