So I have 3 view controllers: TableViewController, A, and B. The user is able to navigate to any view controller from any view controller.
When the user goes back and forth between A, and B view controllers I want them to be pushed onto the nav. stack. When the "home" button is pressed, I would like for the view controllers to all be popped back to the TableViewController using popToViewController, not popToRootViewController (for reasons).
I have partly working code that pops the last visited view controller, but now all the ones in between.
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if indexPath.row == 0 {
if let navController = self.navigationController {
for controller in navController.viewControllers {
if controller is TableViewController {
navController.popToViewController(controller, animated: true)
break
}
}
}
} else {
let vcName = identities[indexPath.row]
let viewController = storyboard?.instantiateViewController(withIdentifier: vcName)
self.navigationController?.pushViewController(viewController!, animated: true)
}
}
I'm not sure why all the view controllers aren't being popped.
Code I use to check what's being pushed and popped:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true)
if self.isMovingToParentViewController {
print("A is pushed")
}
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(true)
if self.isMovingFromParentViewController {
print("A is popped")
}
}
I'm also checking increase in memory.
I will provide more code/info in needed.
Any help would be greatly appreciated.
Your confusion may simply be the way you are trying to "check" that the VCs are "popped".
Suppose you have gone:
root->TableView->A->B->A->B->B->B->`
At that point, the only VC that is visible is the last instance of A. So when you call
navController.popToViewController(controller, animated: true)
viewWillDisappear() will only be called on the last instance of A - none of the other VC instances will "disappear" because they are not visible.
If you want to confirm the other VCs in the stack are being "removed", put this in each view controller:
deinit() {
print("I'm being removed:", self)
}
The other part of the question - do you want to animate through the process? So you would actually see the VCs "walk back up the stack"? If so, follow #FryAnEgg's link to Completion block for popViewController
Try something like this:
var theControllerIWantToPopTo = controllerB // or whatever other condition
if let navController = self.navigationController {
for controller in navController.viewControllers {
if controller is TableViewController {
if controller == theControllerIWantToPopTo {
navController.popToViewController(controller, animated: true)
break
}
}
}
}
Remember, popToViewController will pop all controllers until the chosen one is on top, as opposed to popViewController which will only pop the top controller. If you want to pop them one at a time with animation on each pop see: Completion block for popViewController
Related
I'm new to iOS.
I have an app where the path through the app can vary depending on the configuration I fetch from an API. Because of this, I don't use segues because I would need to create a segue from each ViewController (VC) to EVERY other VC. It creates a mess of segues that I don't want. So Instead I navigate from screen to screen like this:
func navigate(to viewController: String) {
let storyboard = UIStoryboard(name: K.mainStoryBoard, bundle: nil)
let nextVC = storyboard.instantiateViewController(identifier: viewController)
self.present(nextVC, animated: true, completion: nil)
}
My question is this: If I would have embedded my VCs in a NavigationController I know it would have created a stack. When I get to the last screen I would call func popToRootViewController(animated: Bool) -> [UIViewController]? and start from the beginning. However, I don't use a NavigationController. So, does that mean that when I present the next VC it replaces the previous one or does it stack on top of the previous one? I'm trying to prevent memory leaks so I want to make sure I don't keep stacking the VCs on top of each other until my app runs out of memory and crashes.
Thanks in advance
Edit
So, in my final VC I created an unwind segue. And I call it like this: performSegue(withIdentifier: "unwindToMain", sender: self)
and In my first VC (the initial VC in my app) I write this:
#IBAction func unwind( _ seg: UIStoryboardSegue) {
}
Everything works fine the first trip through the app. The last VC unwinds back to the fist VC. The problem is now that when I try to run through the app again (starting from VC 1 and then going to the next one) I now get this error:
MyApp[71199:4203602] [Presentation] Attempt to present <MyApp.DOBViewController: 0x1038760c0> on <MyApp.ThankYouViewController: 0x112560c30> (from <MyApp.ThankYouViewController: 0x112560c30>) whose view is not in the window hierarchy.
To make sense of this, DOBViewController would be the second VC I want to go to from the MainVC. ThankYouViewController is my last VC. It looks as if it isn't completely removed from the stack. Can anyone tell me what's going on?
Here is a very simple, basic example...
The controllers are setup in Storyboard, each with a single button, connected to the corresponding #IBAction.
The DOBViewController has its Storyboard ID set to "dobVC".
The ThankYouViewController has its Storyboard ID set to "tyVC".
MainVC is embedded in a navigation controller (in Storyboard) and the navigation controller is set to Initial View Controller:
class MainVC: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.setNavigationBarHidden(true, animated: false)
}
#IBAction func pushToDOB(_ sender: Any) {
if let vc = storyboard?.instantiateViewController(withIdentifier: "dobVC") as? DOBViewController {
navigationController?.pushViewController(vc, animated: true)
}
}
}
class DOBViewController: UIViewController {
#IBAction func pushToTY(_ sender: Any) {
if let vc = storyboard?.instantiateViewController(withIdentifier: "tyVC") as? ThankYouViewController {
navigationController?.pushViewController(vc, animated: true)
}
}
}
class ThankYouViewController: UIViewController {
#IBAction func popToRoot(_ sender: Any) {
navigationController?.popToRootViewController(animated: true)
}
}
does that mean that when I present the next VC it replaces the previous one or does it stack on top of the previous one?
The new one stacks on top of the previous one.
When you present a view controller, like
self.present(nextVC, animated: true, completion: nil)
The one you called .present on (self in this case) becomes presentingViewController for the nextVC instance.
The one you presented (nextVC in this case) becomes presentedViewController for the self instance.
I have UINavigationController with several pushed view controllers.
UPD: Last pushed controller modally presents another controller.
Also, I have UINavigationControllerDelegate with some logic at navigationController:willShowViewController:animated:.
UPD: Navigation controller is its own delegate. Delegate is set in viewDidLoad method.
Question rises when I try to close all controllers programically from presented view controller:
// Close all controllers in navigation stack
presentingViewController?.navigationController?.popToRootViewController(animated: true)
// Close presented view controller
dismiss(animated: true, completion: nil)
Method navigationController:willShowViewController:animated: is not called. But it is called when I do the same without presented controller (thanks to #donmag for example project where it works).
Searched SO for answers or similar questions, but found nothing, any thoughts?
In your "presenting" VC, you want to implement a delegate/protocol pattern so your "presented" VC can call back and perform the dismiss and popToRoot...
// protocol for the presented VC to "call back" to the presenting VC
protocol dismissAndPopToRootProtocol {
func dismissAndPopToRoot(_ animated: Bool)
}
// in the presenting VC
#IBAction func presentTapped(_ sender: Any) {
if let vc = storyboard?.instantiateViewController(withIdentifier: "presentMeVC") as? PresentMeViewController {
// Assign the delegate when instantiating and presenting the VC
vc.dapDelegate = self
present(vc, animated: true, completion: nil)
}
}
func dismissAndPopToRoot(_ animated: Bool) -> Void {
// this will dismiss the presented VC and then pop to root on the NavVC stack
dismiss(animated: animated, completion: {
self.navigationController?.popToRootViewController(animated: animated)
})
}
// in the presented VC
var dapDelegate: dismissAndPopToRootProtocol?
#IBAction func dismissTapped(_ sender: Any) {
// delegate/protocol pattern - pass true or false for dismiss/pop animation
dapDelegate?.dismissAndPopToRoot(false)
}
Here's a full demo project: https://github.com/DonMag/CustomNavController
From documentation:
popToRootViewControllerAnimated:
Pops all the view controllers on the stack except the root view controller and updates the display.
popViewControllerAnimated:
Pops the top view controller from the navigation stack and updates the display.
So seems like in order to get navigationController:willShowViewController:animated: called every time you have to do subsequent popViewControllerAnimated:, because the display got updated each time after you pop a new controller. When you pop to root view controller, update display is called only once.
I have an mainViewcontroller in that one button called get value.Then i am calling dataviewcontroller to select any item in collection view cell.Once user select any cell.That particular dataviewcontroller will dismiss and while dismiss it will have the user selected item name and it will display in mainViewcontroller .Now the view controller is not dismissing.
Here the code :
In my mainViewcontroller:
var SelectedName: String? = ""
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
currentTF.text = SelectedName
}
Now dataviewcontroller :
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if collectionView.cellForItem(at: indexPath) != nil {
selectedNamesss = allitems[indexPath.row] as String
}
calldismiss()
print(selectedNamesss)
}
func calldismiss() {
if let presenter = presentingViewController as? mainViewcontroller {
presenter.SelectedName = selectedNamesss
}
dismiss(animated: true, completion: nil)
}
Now how can i solve this.My viewcontroller is not dismising and values also not showing.
Thanks in advance ~
My Suggestion is to use "UnwindSegue" technique, which is simply works like "PerformSegue", So you can send back values very easily using prepare for segue and will be back to main controller. Here is tutorial for unwind segue. Or you can find more from google.
For The problem where data is not showing, problem is with this condition:
if let presenter = presentingViewController as? mainViewcontroller {
presenter.SelectedName = selectedNamesss
}
this condition will never be true as the presentingViewController would always be a UINavigationController. so you need to change that may be something like follows:
if let presenter = (presentingViewController as! UINavigationController).viewControllers.first! as? mainViewcontroller {
presenter.SelectedName = name
}
Here i am using viewControllers.first! in condition as mainViewcontroller was at first index of the viewControllers array. Check the viewControllers array and find the index of mainViewcontroller and make changes accordingly.
I made this changes and the code is working for me. I am getting the selected name on the mainViewcontroller.
Also i would like to mention that this is not the right way to transfer data backwards. You can use delegates, blocks or unwind segue to achieve this.
Though this is working i am attaching a gif to show this working.
Regarding the issue where your controller is not being dismissed, i am presenting the controller in following way:
let dataVC = self.storyboard?.instantiateViewController(withIdentifier: "dataviewcontroller") as! dataviewcontroller
self.present(vc, animated: true, completion: nil)
Try this and see if this helps you :)
I'm developing a shopping list like app where I have a Navigation Controller and the root view controller is the screen where the user can search for products (SearchViewController). When the user selects a product it segues to DetailViewController. This view has an option to check out or add more products. If users click on "Add more products" I have to segue to SearchViewController so they can search for more products. I want to present this VC again but I want the Nav Bar to show this time since I want to be able to go back if I decide not to add any other products.
Right now I'm sending the shoppingContext in the segue to determine from the SearchVC if I come from "DetailsVC" or not.
I think there's a problem with the way I'm adding view controllers to the navigation stack, but I've never encountered a problem like this and don't know what else to try.
With my current implementation (performSegue from DetailsVC to SearchVC) any time I click on a new item it segues twice to the Details screen, which I suspect may also be caused by the same navigation stack issue.
I tried creating a new object of SearchVC and pushing it to the stack instead of performing the segue but it didn't work either.
What can I do to fix it?
Basically, in detailsVC I do the following:
let segueAction = SegueAction(name: "segueToSearch", preparer: {
destinationVC in
if
let activeVC = destinationVC as? SearchViewController
{
activeVC.shoppingList = self.shoppingViewModel.shoppingList
}
})
performSegue(withIdentifier: segueAction.name, sender: segueAction)
The segue "segueToSearch" is a Show (push) type segue.
Then in the SearchVC I check if shoppingList != nil and if so do:
navigationController?.setNavigationBarHidden(false, animated: false)
If I check if the navigation bar is hidden it returns false but I still don't see it.
Hi it's pretty straight forward. Answer can be found here:
Navigation bar show/hide
[[self navigationController] setNavigationBarHidden:NO animated:YES];
And I would put a property to check in the viewWillApear.
-- EDIT: --
TESTED: I added it to a button action, works also in the viewDidAppear when dismiss back from to detail.
Hope it helps.
class ViewController: UIViewController {
var didHideNav: Bool = false
#IBAction func changeHidden(_ sender: UIButton) {
if !didHideNav {
print("Should Be Hidden")
self.navigationController?.setNavigationBarHidden(true, animated: true)
didHideNav = true
}else{
print("Should Be Visible")
self.navigationController?.setNavigationBarHidden(false, animated: true)
didHideNav = false
}
}
override func viewDidAppear(_ animated: Bool) {
if !didHideNav {
print("Should Be Hidden")
self.navigationController?.setNavigationBarHidden(true, animated: true)
didHideNav = true
}else{
print("Should Be Visible")
self.navigationController?.setNavigationBarHidden(false, animated: true)
didHideNav = false
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
In my app I have three table view controllers and then potentially many UIViewControllers each of which has to lead back to the first table view controller if the user presses back at any point. I don't want the user to have to back through potentially hundreds of pages. This is what I amusing to determine if the user pressed the back button and it works the message is printed
override func viewWillDisappear(_ animated: Bool) {
if !movingForward {
print("moving back")
let startvc = self.storyboard!.instantiateViewController(withIdentifier: "FirstTableViewController")
_ = self.navigationController!.popToViewController(startvc, animated: true)
}
}
I have searched and none of the solutions have worked so far.
popToViewController not work in a way you are trying you are passing a complete new reference of FirstTableViewController instead of the one that is in the navigation stack. So you need to loop through the navigationController?.viewControllers and find the FirstTableViewController and then call popToViewController with that instance of FirstTableViewController.
for vc in (self.navigationController?.viewControllers ?? []) {
if vc is FirstTableViewController {
_ = self.navigationController?.popToViewController(vc, animated: true)
break
}
}
If you want to move to First Screen then you probably looking for popToRootViewController instead of popToViewController.
_ = self.navigationController?.popToRootViewController(animated: true)
Try this :
let allViewController: [UIViewController] = self.navigationController!.viewControllers as [UIViewController];
for aviewcontroller : UIViewController in allViewController
{
if aviewcontroller .isKindOfClass(YourDestinationViewControllerName)// change with your class
{
self.navigationController?.popToViewController(aviewcontroller, animated: true)
}
}
If you are in a callback, particularly an async network callback, you may not be on the main thread. If that's you're problem, the solution is:
DispatchQueue.main.async {
self.navigationController?.popToViewController(startvc, animated: true)
}
The system call viewWillDisappear() is always called on the main thread.