Resetting the navigation stack on a UITabBarController doesn't work - ios

I'm trying to reset the navigation stack on a subclass of UITabViewController embedded in a UINavigationController but it doesn't work.
My navigation stack, which I create programmatically, is like this:
UINavigationController => ControllerA (a subclass of UIViewController) =>
ControllerB (a subclass of UIViewController) => ControllerC (a
subclass of UITabBarController).
When users press on the "Back" button or swipe back from ControllerC, the app should go back to ControllerA, not ControllerB.
Usually, when I want to reset the navigation stack, I do this in the Controller's viewDidLoad() method:
override func viewDidLoad() {
super.viewDidLoad()
// usually work, but not in a subclass of UITabBarController as self.navigationController is nil
if let navigationController = self.navigationController {
// keep only the root controller (0) and the current controller
navigationController.viewControllers = [navigationController.viewControllers[0], self]
}
}
but this doesn't work in ControllerC (the subclass of UITabViewController) as self.navigationController is nil.
If I do this instead (still in ControllerC's viewDidLoad() method):
/// ControllerC's viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
if let navigationController = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController {
// keep only the root controller (0) and the current controller
navigationController.viewControllers = [navigationController.viewControllers[0], self]
}
}
This works, but then there is no animation between ControllerB and ControllerC when I do:
controllerB.navigationController?.pushViewController(ControllerC(), animated: true)
I also tried to override ControllerC's viewWillDisappear() method:
/// ControllerC's viewWillDisappear
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
if self.isMovingFromParent {
if let navigationController = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController {
navigationController.popToRootViewController(animated: true)
}
}
This works, but ControllerB is briefly visible before ControllerA is shown.
Any help would be greatly appreciated!

In the ControllerC instead of trying to override viewWillDisappear() method you can override viewDidAppear() like that:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if let navC = UIApplication.shared.keyWindow?.rootViewController as? UINavigationController {
// keep only the root controller (0) and the current controller
navC.viewControllers = [navC.viewControllers[0], self]
}
}
And ControllerB won’t be briefly visible before ControllerA when you navigate backwards.

Related

Swift initialize tabBarController without presenting it

The initial view controller is the tab bar controller. The tab bar is created programmatically.
Problem:
I need to load the TabBarViewController without presenting it if the boolean value loggedIn == false.
On older devices, this code works fine to never show the TabBarViewController if the user is not logged in.
However, on newer/faster devices the tab bar flashes before presenting the LogInViewController().
final class TabBarViewController: UITabBarController {
// MARK: - Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
configureTabs()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if(!loggedIn){
let loginVC = LogInViewController()
loginVC.isModalInPresentation = true
loginVC.modalPresentationStyle = .fullScreen
present(loginVC, animated: false)
}
}
}
What is the best work around to still load the TabBarViewController but not show it if I need to present a new VC.

How to transition out of parent UINavigationController

I have the following code, which setups and shows a UIViewController to be embedded into a UINavigationController:
private func transitionToMainVC() {
let vc = UINavigationController(rootViewController: SpacesVC())
DispatchQueue.main.async {
self.show(vc, sender: self)
}
}
Now, inSpacesVC I want to show() another UIViewController, but outside of the "parent" UINavigationController.
When I use the following code in SpacesVC:
// Called by a button touch up inside
#objc private func transitionToMainVC() {
let vc = NextVC()
self.show(vc, sender: self)
}
It transitions to NextVC but it shows the navigation bar at the top of the screen; i.e. the new view controller is still embedded under the UINavigationController defines in the first snippet.
I am aware of the possibility of hiding the navigation bar in NextVC as such:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
navController.isNavigationBarHidden = true
}
But I want to show() the NextVC without embedding it in the navigation controller, since I won't need that anymore. How can I do that?
Use this method instead of show.
func present(_ viewControllerToPresent: UIViewController,
animated flag: Bool,
completion: (() -> Void)? = nil)

Does 'pushViewController' Prevent the ViewController from Calling deinit?

I have several view controllers (UIViewController). Each view controller has its own storyboard. And I want the app to call deinit when it transitions from one view controller to another. But it won't.
The app starts with HomeViewController. And it will transition to SelectViewController when the user taps a button (UIButton).
class HomeViewController: BasicViewController {
#IBAction func selectTapped(_ sender: UIButton) {
let storyboard = UIStoryboard(name:"Select",bundle:nil)
let controller = storyboard.instantiateViewController(withIdentifier: "SelectView") as! UINavigationController
let viewController = controller.topViewController as! SelectViewController
self.navigationController?.pushViewController(viewController, animated: true)
}
deinit {
print("HOME HAS BEEN REMOVED FROM MEMORY")
}
}
The app never calls deinit when it leaves HomeViewController. I wonder if that's because it's pushing a view controller? If I set the view controller as follows, the app will crash. What am I doing wrong? I have never done it with different storyboards. Thanks.
class HomeViewController: BasicViewController {
#IBAction func selectTapped(_ sender: UIButton) {
let storyboard = UIStoryboard(name:"Select",bundle:nil)
let viewController = storyboard.instantiateViewController(withIdentifier: "SelectView") as! SelectViewController
self.navigationController?.setViewControllers([viewController], animated: true)
}
}
UPDATE 1
I guess it's just the following.
#IBAction func selectTapped(_ sender: UIButton) {
let storyboard = UIStoryboard(name:"Select",bundle:nil)
let controller = storyboard.instantiateViewController(withIdentifier: "SelectView") as! UINavigationController
self.present(controller, animated: true, completion: nil)
}
UPDATE 2
I also clear the view controller stack when SelectViewController appears as follows.
override func viewWillAppear(_ animated:Bool) {
super.viewWillAppear(animated)
var navigationArray = self.navigationController?.viewControllers
navigationArray?.removeAll()
}
Actually, It is not about storyboards. As per the Apple's documentation, "deinit" method gets automatically called when an object is deallocated from memory, not when a view controller is pushed from current one.
When you push "SelectViewController" from "HomeViewController", instance of "HomeViewController" does not deallocated from memory, hence deinit method does not called.
Here is a link to Apple's Documentation for Deinitialization. Alternatively, you can use "viewDidDisappear" method of the view controller to perform an operation if it satisfies your need.
From your code snippet, I understand that you expect deinit is called after pushViewController. But, by calling pushViewController, your navigation controller pushes your HomeViewContoller to navigation stack, which means navigation controller holds strong reference to your HomeViewController object. So, deinit is not called. If you don't need HomeViewController, you have to manually remove it from navigation stack w/in SelectViewController.
See this how. How to remove a specific view controller from uinavigationcontroller stack?
In SelectViewController,
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let allControllers = NSMutableArray(array: navigationController!.viewControllers)
allControllers.removeObject(at: allControllers.count - 2)
self.navigationController!.setViewControllers(allControllers as [AnyObject] as! [UIViewController], animated: false)
}

Setting up a UITabBarController with nib ViewControllers

Ok stuck on this one, help would be most appreciated.
So I have a tab controller inside a navigation controller in my storyboard.
Then I have two view controllers with nib files that I want to load into the tab controller programmatically. I'm using the following code but getting a blank screen when I load the app.
class MainTabBarController: UITabBarController, UITabBarControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
override func viewDidAppear(animated: Bool) {
super.viewWillAppear(animated)
let verbViewController = VerbViewController(nibName: "VerbViewController", bundle: nil)
let communityViewController = CommunityViewController(nibName: "CommunityViewController", bundle: nil)
let tabIcon1 = UITabBarItem(title: nil, image: UIImage(named: "VerbTab"), tag: 0)
let tabIcon5 = UITabBarItem(title: nil, image: UIImage(named: "CommunityTab"), selectedImage: nil)
verbViewController.tabBarItem = tabIcon1
communityViewController.tabBarItem = tabIcon5
let tabControllers = [verbViewController, communityViewController]
self.tabBarController?.setViewControllers(tabControllers, animated: true)
self.viewControllers = tabControllers
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
// MARK: - Delegate method
func tabBarController(tabBarController: UITabBarController, shouldSelectViewController viewController: UIViewController) -> Bool {
return true
}
}
You are calling self.tabBarController from within a UITabBarController which doesn't make much sense.
UIViewController's tabBarController returns "The nearest ancestor in the view controller hierarchy that is a tab bar controller." which in your case is probably nil because your tab bar controller is not contained in yet another tab bar controller.
You probably want to just call self.setViewControllers directly.
This behavior would also probably make more sense in viewDidLoad rather than viewWillAppear, do you really want to replace the controllers for each tab every time the tab bar appears?

Determine viewWillAppear from Popped UINavigationController or UITabBarController

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 ...
}
}

Resources