「Navigationbarで遷移をくり返した状態から、特定のViewControllerを削除するにはどうしたらいいか?」を解決します。
動作イメージ
行き(PUSH)
帰り(POP)
この問題を解消するには、どうすれば良いか悩みました。
結論:setViewControllers(_:animated:)を使う
NavigationControllerのsetViewControllers(_:animated:)を使用することで、この問題は解消できます。
実装例
func goToNextViewController() {
let nextViewController = NextViewController()
var newNavigationViewControllers = navigationController?.viewControllers.filter {
!($0 is RemoveViewController)
} ?? []
newNavigationViewControllers.append(nextViewController)
navigationController?.setViewControllers(newNavigationViewControllers, animated: true)
}
動作例
結論に至るまでの試行錯誤から学ぶ
ここからは、結論に至るまでに辿った手順を踏みながら、うまくいかないケースを挙げます。
NavigationController.viewControllersプロパティに着目
UINavigationControllerにはviewControllers
プロパティがあります。
var viewControllers: [UIViewController] { get set }
このプロパティはUIViewControllerの配列型であり、遷移基のViewController
が順番にスタックされます。
冒頭の動作イメージで挙げた「START」「REMOVE」「END」の画面を例に考えた場合、次のようになります。
イメージ
検証例
func checkStackViewControllers() {
guard let navigationController else { return }
let count = navigationController.viewControllers.count
for i in 0..<count {
let viewController = navigationController.viewControllers[i]
if i == 0 {
print("先頭は \(viewController.title!) です。")
} else {
let previousViewController = navigationController.viewControllers[i - 1]
print("\(viewController.title!) は \(previousViewController.title!) の上に積まれています。")
}
}
}
動作例
先頭は Start です。
Remove は Start の上に積まれています。
End は Remove の上に積まれています。
配列で管理されているならRemoveできる
viewControllersはsetも可能です。
参照も更新も可能なら、対象のindexが特定できればRemoveが可能と考えました。
イメージ
検証例
removeStackViewControllerをEND画面でnextボタンをタップした際に対応してみました。
func removeStackViewController() {
guard let navigationController = navigationController else { return }
if let viewController = navigationController.viewControllers.last,
let previousViewController = navigationController.viewControllers.dropLast().last {
print("\(viewController.title!) は \(previousViewController.title!) の上に積まれています。")
}
if let index = navigationController.viewControllers.firstIndex(where: { $0 is RemoveViewController }) {
navigationController.viewControllers.remove(at: index)
}
if let updatedViewController = navigationController.viewControllers.last,
let updatedPreviousViewController = navigationController.viewControllers.dropLast().last {
print("\(updatedViewController.title ?? "タイトルなし") は \(updatedPreviousViewController.title ?? "タイトルなし") の上に積まれています。")
}
let startViewController = StartViewController()
navigationController.pushViewController(startViewController, animated: true)
}
動作例
End は Remove の上に積まれています。
End は Start の上に積まれています。
Removeの不都合
ここまでは、隠れているRemove画面を削除のみでしたが、さらにEND画面の削除も追加した場合はどうでしょうか?
イメージ
検証例
@objc func removeStackViewController() {
guard let navigationController = navigationController else { return }
navigationController.viewControllers.removeAll { viewController in
viewController is RemoveViewController || viewController is EndViewController
}
let startViewController = StartViewController()
navigationController.pushViewController(startViewController, animated: true)
}
問題に遭遇
表示中の画面を、次の画面への遷移中にRemoveした場合、次の問題が生じました。(正確にはnexボタンのタップ)
- 即時反映で不自然な切り替えになる
- Removeは配列の操作は可能だがアニメーションがない
次の動作例のように、ただ画面が削除されて切り替わるため、不自然な挙動になってしまいます。
動作例
遷移中でのRemove処理では、問題が生じてしまいます。
そのため、不自然な挙動が見えないように「END画面が隠れてから処理を実行」を意識する必要があります。
Removeの扱いには工夫が必要
実際に「画面が隠れてから処理の実行」を検討します。
検討するにあたり、画面の遷移とライフサイクルを整理します。
画面が隠れるまでの流れ
- 「Nextをタップ」で画面遷移が開始: UINavigationControllerの
pushViewController
によりEND画面から次(START画面)への遷移が始まる - アニメーション前: START画面がメモリに読み込まれ
viewDidLoad
がコールされる - アニメーション開始: END画面で
viewWillDisappear
がSTART画面でviewWillAppear
がコールされる - アニメーション進行中:
pushViewController
のアニメーションが実施される - アニメーション完了: END画面で
viewDidDisappear
がSTART画面でviewDidAppear
がコールされる✅
「END画面が隠れる」のは、アニメーション完了のタイミングになります。
END画面のviewDidDisappear
or START画面のviewDidAppear
で、先に述べたremoveStackViewController()を扱うと良さそうです。
検証例
class EndViewController: UIViewController {
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
removeStackViewController()
}
private func removeStackViewController() {
guard let navigationController = navigationController else { return }
navigationController.viewControllers.removeAll { viewController in
viewController is RemoveViewController || viewController is EndViewController
}
}
}
START画面のviewDidDisappear
class StartViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
removeStackViewController()
}
private func removeStackViewController() {
guard let navigationController = navigationController else { return }
navigationController.viewControllers.removeAll { viewController in
viewController is RemoveViewController || viewController is EndViewController
}
}
}
動作例
END画面からSTART画面への遷移が綺麗になりました。
しかしRemoveを使った場合、UINavigationControllerが持つ特定のViewControllerを削除するには、考慮するべき点が多くなることもわかりました。
コメント