I have the method to check when the back button in navigation bar is press and the method go back to root page but for some reason when self.navigationController?.popToRootViewController(animated: true) it only go back to the previous page. do anyone know how to go back to the root when navigation bar back button is pressed?
override func didMove(toParentViewController parent: UIViewController?) {
super.didMove(toParentViewController: parent)
if parent == nil{
self.navigationController?.popToRootViewController(animated: true)
}
}
In this question he is asking how to what method can he use to customise his back button. In my code its able to detect when user press on back button and self.navigationController?.popToRootViewController(animated: true)
is suppose to bring the page back to the root page, however there are somethings in the system preventing my app to go back to the root page.
i think the best way is to create your own custom back button at this page
override func viewDidLoad {
super.viewDidLoad()
navigationItem.hidesBackButton = true
let newBackButton = UIBarButtonItem(title: "Back", style: UIBarButtonItemStyle.plain, target: self, action: #selector(YourViewController.back(sender:)))
navigationItem.leftBarButtonItem = newBackButton
}
func back(sender: UIBarButtonItem) {
// Perform your custom actions
// ...
// Go back to the root ViewController
_ = navigationController?.popToRootViewController(animated: true)
}
credit to this answer by 'fr33g' : Execute action when back bar button of UINavigationController is pressed
Personally I would not recommend what you are trying to achieve, but anyways here is a different solution without customizing the back button.
Steps to implement
Create CustomNavigationController by subclassing
UINavigationController
Override popViewController(animated:)
When ViewController conforms to Navigationable and
shouldCustomNavigationControllerPopToRoot() returns true, call super.popToRootViewController
Otherwise proceed with normally popping the ViewController
Source Code
Custom Navigation Controller
import UIKit
class CustomNavigationController: UINavigationController {
// MARK: - Initializers
override init(rootViewController: UIViewController) {
super.init(rootViewController: rootViewController)
initialSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialSetup()
}
// MARK: - Setups
private func initialSetup() {
// DISCLAIMER: This code does not support `interactivePopGestureRecognizer`, therefore we disable it
interactivePopGestureRecognizer?.delegate = nil
}
// MARK: - Overrides
override func popViewController(animated: Bool) -> UIViewController? {
if shouldNavigationPopToRoot {
return super.popToRootViewController(animated: animated)?.last
}
return super.popViewController(animated: animated)
}
// MARK: - Helpers
private var shouldNavigationPopToRoot: Bool {
return (topViewController as? Navigationable)?.shouldCustomNavigationControllerPopToRoot() == true
}
}
View Controller conforming to Navigationable
import UIKit
protocol Navigationable: class {
func shouldCustomNavigationControllerPopToRoot() -> Bool
}
class ViewController: UIViewController, Navigationable {
// MARK: - Protocol Conformance
// MARK: Navigationable
func shouldCustomNavigationControllerPopToRoot() -> Bool {
return true
}
}
Output
Related
I'm working on adding keyboard shortcuts on my application. There is a view controller that presents another controller:
class ViewController: UIViewController {
override var canBecomeFirstResponder: Bool { true }
override func viewDidLoad() {
super.viewDidLoad()
addKeyCommand(UIKeyCommand(
input: "M",
modifierFlags: .command,
action: #selector(ViewController.handleKeyCommand),
discoverabilityTitle: "Command from the container view"
))
}
#objc func handleKeyCommand() {
present(ModalViewController(), animated: true)
}
override func canPerformAction(
_ action: Selector, withSender sender: Any?
) -> Bool {
if action == #selector(ViewController.handleKeyCommand) {
return isFirstResponder
}
return super.canPerformAction(action, withSender: sender)
}
}
class ModalViewController: UIViewController {
override var canBecomeFirstResponder: Bool { true }
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
addKeyCommand(UIKeyCommand(
input: "D",
modifierFlags: .command,
action: #selector(ModalViewController.handleKeyCommand),
discoverabilityTitle: "Command from the modal view"
))
if !becomeFirstResponder() {
print("⚠️ modal did not become first responder")
}
}
#objc func handleKeyCommand() {
dismiss(animated: true)
}
}
Both define shortcuts. When the modal view controller is presented, the Discoverability popup includes shortcuts for both presenting and presented view controller. Intuitively, only the modal view controller shortcuts should be included (we are not supposed to be able to interact with the presenting view controller, right?)
I can fix this by overriding the presenting view controller's keyCommands property, but is this a good idea?
In general, what is the reason behind this behavior? Is this a bug or a feature?
UPDATE: Added the canPerformAction(_:sender:) to the presenting view controller (as suggested by #buzzert), but the problem persists.
This is happening because the presenting view controller (ViewController) is your ModalViewController's nextResponder in the responder chain.
This is because the OS needs some way to trace from the view controller that's currently presented on screen all the way back up to the application.
If your presenting view controller only has commands that make sense when it is first responder, the easiest way to resolve this is by simply overriding canPerformAction(_:) on ViewController, and return false if it is not first responder.
For example,
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if self.isFirstResponder {
return super.canPerformAction(action, withSender: sender)
} else {
return false
}
}
Otherwise, if you want more control over the nextResponder in the responder chain, you can also override the nextResponder getter to "skip" your presenting view controller. This is not recommended though, but serves as an illustration of how it works.
ref to #buzzert, i use this code in root viewcontroller, it seems work fine.
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if sender is UIKeyCommand{ /* keyboard event */
if self.presentedViewController != nil{ /* modal now */
return false
}
}
return super.canPerformAction(action, withSender: sender)
}
Currently I have a view controller that when you tap on the tab at index 3 (Map Tab) it loads a view controller that contains a map (Map A). NOTE: it should always load Map A every time the Map Tab is pressed. There is a chiclet at index 0 on the main tab that when tapped allows you to switch from Map A to Map B and then takes you to that map (This switch is done manually so to keep the tab bar on screen using the tab bar's selectedIndex which means the viewWillAppear() doesnt seem to be called). NOTE: Map A and Map B share the same viewController, a bool is used to differentiate which one to load in viewWillAppear..The issue I'm having is after the chiclet is pressed to switch from Map A to Map B, once I hit the Map Tab again on the tab bar it automatically loads the current map I was just on (Map B), but as stated earlier, when pressed from tab bar, it should only load Map A.
This is what I was trying but it still won't show the proper map after the tab has been pressed:
class MainTabBarController: UITabBarController {
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
switch viewController {
case is MapViewController:
let nc = NotificationCenter.default
nc.post(name: Notification.Name("changeMapStatus"), object: nil)
}
}
class MapViewController: BaseViewController {
var mapBSelected: Bool = false
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
mapBSelected ? setupMapB() : setupMapA()
}
#objc func changeMapStatus() {
guard let mainTabController = tabBarController as? MainTabBarController else { return }
mainTabController.refreshMapTab()
self.MapBSelected = false
}
}
func refreshMapTab() {
let index = PrimaryFeatureTab.map.displayOrder// enum to determine tab order
DispatchQueue.main.async {
self.viewControllers?.remove(at: index)
self.viewControllers?.insert(PrimaryFeatureTab.map.rootViewController, at: index)
}
}
}
You can do the following
Create a closure in your FirstViewController like this:
enum MapType {
case mapA
case mapB
}
class MainViewController: UIViewController {
var mapSelectionClosure: ((MapType) -> Void)?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func aTapped(_ sender: Any) {
mapSelectionClosure?(.mapA)
}
#IBAction func bTapped(_ sender: Any) {
mapSelectionClosure?(.mapB)
}
}
Then you can set this closure in your MainTabBarController like this
class MainTabBarController: UITabBarController {
override func viewDidLoad() {
super.viewDidLoad()
guard let mainViewController = viewControllers?.first(where: { $0 is MainViewController }) as? MainViewController else {
return
}
mainViewController.mapSelectionClosure = { mapType in
self.setMapType(mapType)
}
}
func setMapType(_ type: MapType) {
guard let mapViewController = viewControllers?.first(where: { $0 is MapViewController }) as? MapViewController else {
return
}
mapViewController.selectedMapType = type
}
}
In MapViewController...
class MapViewController: BaseViewController {
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.setupMapA()
}
}
Now, when you leave the Map Tab, it will reset itself to Map A.
No need for any code in a custom TabBarController.
I have programmed UIBarButtonItem so that it does some action before switching to a previous view. I was wondering how do I get the viewcontroller of that transitioning scene from my UIBarButtonItem?
So, scene 1 -> scene 2 (current scene) -> scene 1 (after clicking the UIBarButtonItem button)
I've tried to pass the previous scene variables (that I need) to the current scene to perform action on (sense I don't think the transitioning scene is instantiating a new view, but that doesn't work
override func viewDidLoad() {
super.viewDidLoad()
loadTuple()
let addButton: UIBarButtonItem = UIBarButtonItem(title: "Save", style: .plain, target: self, action: #selector(saveExercise(_: )))
self.navigationItem.setRightBarButton(addButton, animated: true)
}
#objc func saveExercise(_ sender: UIBarButtonItem) {
self.addNewTupleToDB(element: self.getNewTuple())
self.saveData()
debugPrint("saveExercise")
self.exerciseVCTableView?.reloadData() // tried to pass the table view from the previous scene to call here
self.navigationController?.popViewController(animated: true)
// Want to save reload the table data of the scene this button transitions to
}```
You may use delegate pattern for solving this. Delegate pattern is something, to delegate some work to other and return to the work after delegation is done.
Suppose ViewController1 has UIBarButton , goes to ViewController2, some function done and return to ViewController1
let us take a protocol
protocol MyProtocol {
func myFunction()
}
then in ViewController2 add a delegate method. Assuming in ViewController2, you have to call a method doMyWork and some work will be done here, then you have to pop.
class ViewController2 {
var delegate : MyProtocol?
override func viewDidLoad() {
super.viewDidLoad()
doMyWork()
}
func doMyWork() {
// my works
delegate?.myFunction()
self.navigationController.popViewController()
}
}
now the viewController1 have to receive the delegate work has done.
in viewController1, in barButtonItem
class ViewController1 {
#objc func barButton(_sender : UIBarButton) {
let viewController = ViewController2()
viewController.delegate = self
self.naviagtionController.pushViewController(viewController, animated : true)
}
}
now you have to implement protocol method
extension ViewController1 : MyProtocol {
func myFunction() {
self.tableView.reloadData()
}
}
I'd like to have a close button on each view controller that appears in the navigation stack. I've read here that I need to create an object that is a uinavigationdelegate, I think this object will have a method like didTapCloseButton?
Questions:
Should I create a protocol and make everything confirm to it, i.e.:
protocol CustomDelegate: UINavigationControllerDelegate {
func didTapCloseButton()
}
public class ViewController: CustomDelegate {
func didTapCloseButton() {
//not sure what goes in here?
}
}
How do I get the close button to show on the navigation bars of every view?
When the user clicks the close button, how do I get that to dismiss every view on that stack?
Thanks for your help!
Here a simple solution. Create UINavigationController subclass and override pushViewController method.
class NavigationController: UINavigationController {
override func pushViewController(_ viewController: UIViewController, animated: Bool) {
super.pushViewController(viewController, animated: animated)
let closeBarButtonItem = UIBarButtonItem(
title: "Close",
style: .done,
target: self,
action: #selector(self.popViewController(animated:)))
viewController.navigationItem.rightBarButtonItem = closeBarButtonItem
}
}
Not sure if this is what you intended but you can do:
protocol CustomDelegate: UINavigationControllerDelegate {
func didTapCloseButton()
}
extension CustomDelegate where Self : UIViewController{
func didTapCloseButton(){
// write your default implementation for all classes
}
}
now for every UIViewController class you have you can just do :
class someViewController: CustomDelegate{
#IBAction buttonClicked (sender: UIButton){
didTapCloseButton()
}
}
I am unable to find a way to distinguish between popping from the Nav controller stack and entering the view controller from the UITabBarController.
I want to call a method in ViewWillAppear only when the view is presented from the TabBar, not when someone presses back in the navigation controller.
If I wasn't using a TabBarController, I could easily get this functionally using viewDidLoad.
I've tried,
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
println("View Will Appear")
if isBeingPresented() {
println("BP")
}
if isMovingFromParentViewController() {
println("from")
}
if isMovingToParentViewController() {
println("to")
}
}
But there is no difference when I present from pressing the Tab Button or when press back button.
Only the "View Will Appear" is getting called.
Using iOS 8.4 / Swift
Sounds like a good use of the UITabBarControllerDelegate.
First, add a Bool property on your ViewController comingFromTab:
class MyViewController: UIViewController {
var comingFromTab = false
// ...
}
Set your UITabBarControllerDelegate to whatever class you want and implement the method shouldSelectViewController. You may also want to subclass UITabBarController and put them in there.
func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {
if let myViewController = viewController as? MyViewController {
myViewController.comingFromTab = true
}
If your tab's initial view controller is a UINavigationController, you will have to unwrap that and access it's first view controller:
if let navController = viewController as? UINavigationController {
if let myViewController = navController.viewControllers[0] as? MyViewController {
// do stuff
}
}
Lastly, add whatever functionality you need in viewWillAppear in your view controller:
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
// ...
if comingFromTab {
// Do whatever you need to do here if coming from the tab selection
comingFromTab = false
}
}
There is no way to know for sure. So I guess the easiest way is to add some variable that you will have to change before popping back to that view controller and checking it's state in viewWillAppear.
class YourViewController: UIViewController {
var poppingBack = false
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if !poppingBack {
// your logic
}
else {
poppingBack = false // reset it for next time
}
}
}
// somewhere else in code, suppose yourVC is YourViewController
yourVC.poppingBack = true
self.navigationController.popToViewController(yourVC, animated: true)
You can also try implementing UINavigationControllerDelegate's - navigationController:willShowViewController:animated: method and check if it will be called when presenting your view controller from tab bar.
You can check parentViewController property
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
if parentViewController is UITabBarController {
// Presented by UITabBarController
} else if parentViewController is UINavigationController {
// Presented by UINavigationController
} else {
// Presented by ...
}
}