NavigationBar の戻るボタンをタップしたら、特定の VC に戻りたい!


TL;DR

途中の VC をスタックから外せばいい。

本編

例えばこんなこと、考えたことありませんか?まず画面 A から Push 遷移で画面 B へ、次に画面 B から同じく Push 遷移で画面 C へ遷移しました。このあと、画面 C から戻る操作で B を飛ばして直接画面 A に戻りたい!よくあるのは、例えば画面 B が二段階認証の画面の時、この場合画面 C から戻る場合二段階認証の画面に戻るのはナンセンスなので、その画面を飛ばして前の画面に戻りたい時ですね。

【効果なし】 backBarButtonItem をカスタマイズ

まず考えられるのは NavigationBar の戻るボタンをカスタマイズし、このボタンがタップされたら、自分で navigationController?.popToViewController を実行する方法ですね。しかし残念ながら、NavigationBar の戻るボタンは leftBarButtonItem ではなく backBarButtonItem です。このボタンの動作をカスタマイズするのはできないのです。

あれ?違うよ?navigationItem.backBarButtonItem{ get set } 対応だよ?

はい、そうです。しかし、ここで設定できるのは、次の画面に遷移された時の戻るボタンです。つまり、これは戻り先が自分の時の設定です。さらに backBarButtonItemaction を設定しても、UIKit には無視されます。ここで設定できるのはあくまで backBarButtonItem の表示だけです。

ちなみに UINavigationControllerDelegate に適合し、navigationController?.delegate を自分自身に設定することで戻るボタンの動作のカスタマイズが可能との記事も散見しますが、アレは嘘です。できません。そもそも UINavigationControllerDelegate は遷移先のカスタマイズの責務まで持ちません。

【イマイチ】 leftBarButtonItem を設定

戻るボタンのカスタマイズができないなら、次に考えるのは、leftBarButtonItem で置き換える方法です。確かに、これなら popToViewController が実行可能なので、特定の VC に戻れます。しかし、デメリットも二つあります。

一つ目は戻るボタン特有の < マークが表示されないことです。これは UIKit 共通のマークですので、これをなくすと一貫性がなくなり、特に iOS のヘビーユーザに違和感を覚えられることになります。もちろん無理やり独自で < 画像を作って表示させてあげることも可能は可能ですが、やはり純正の < マークと比べて何か違和感があります。

もう一つは左端からのエッジスワイプによる戻る操作ができなくなることです。これも UIKit 共通のジェスチャー操作であり、特に大画面の iPhone が主流になった今日では必要不可欠と言っても過言ではない仕様の一つです。これがなくなったら、前の画面に戻りたいときは非常に押しづらい左上のバックボタンを押すしかなくなります。もちろんこれも一応独自でエッジスワイプジェスチャーを実装してあげることも可能は可能ですが、やはり面倒くさいです。

そして上記のデメリット以外に、設計によっては画面 A のタイトルを画面 C の leftBarButtonItem で設定しないといけないので、微妙に Fat になったりするのも気持ち悪いですね。

【オススメ】遷移後に飛ばしたい画面をスタックから削除

実はアプローチを変えてみれば、これは意外と非常に簡単に解決できる方法があります:飛ばしたい画面をそもそも navigationController?.viewControllers 配列から削除しちゃえばいいのです。そもそもの話、画面 B を戻るときに飛ばしたいってことは、画面 B はもう要らないってことになります。でしたら、もう画面 C に遷移したら、一つ前の画面 B を削除しちゃえば、NavigationBar が勝手に画面 A に戻すように設定してくれますので、< や画面名の表示とかエッジスワイプのジェスチャー設定とかの面倒な作業は一切やらなくていいので、UIKit の素直な実装で動いてくれるからとても気持ちいいです。

ViewControllerB
    navigationController?.pushViewController(vc, animated: true) // 次の画面 C への Push 遷移
    navigationController?.viewControllers.removeAll(where: { $0 === self }) // その直後に自身を NavigationController の viewControllers スタックから削除

もちろんこれが完璧と言うわけでもありません、一つだけ細かい問題があります:それは画面 C に遷移した直後に、一瞬だけ画面 B の名前が戻るボタンの場所に表示されることです。表示は一瞬だけで、遷移が終わったら画面 B はスタックから消えるので、画面 A の名前に変わります。

【オルタネイティブ】そもそも Push 遷移しない

ちなみに、アップルの公式アプリでも似たような処理があります、それは設定アプリからパスコードを設定する画面に入るところです。この画面も、パスコードの設定画面に入る前に一回認証処理が挟まり、戻るとき当然ながらその認証画面には行かないで設定一覧画面に戻ります。ではアップルがどうしてるかと言うと、認証画面は Push 遷移ではなく Modal 遷移です、そして認証が通ったら裏でパスコード画面に Push 遷移済みの状態にした上で認証画面を Dismiss します。

このような処理も、見た目として特に違和感なく自然に見えますが、ただしそもそもの話、設定一覧画面から表示されている「遷移先」が「パスコード設定」であり、「認証」ではありません。だからこそ認証画面を Modal 遷移しても特に違和感を覚えません。ユーザは「パスコード設定画面」を「Push 遷移」で期待しているからです。しかし逆に遷移先を「画面 B」と記載しながら Modal 遷移すると、違和感を覚えるでしょう。

後書き

難しそうな機能でも、ときにはちょっとアプローチを変えるだけで、すんなり解決することもあります。