Clearing Navigation Stack in Swift - ios

I'm currently designing an application that has a funnel flow and a dashboard flow. I'd like the funnel flow to be cleared from memory on completion:
So if it goes
1) if new user start funnel -> 2) funnel screens 1-5 -> 3) funnel complete screen
I'd like to transition to dashboard screen which is not yet on the stack (it's not my head controller in this case). How can I clear the 6 screens above from memory when I transition to dashboard - basically setting a new navigation root and clearing the rest? An unwind segue doesn't appear to be able to set a new root destination.

If you only want to clear the navigation stack, and push a new view on top of it, there is an even simpler solution.
Let's suppose you already (programmatically) assigned a navigation controller, e.g. in a viewDidLoad() function, like:
let navController = UINavigationController( rootViewController: YourRootController )
view.addSubview( navController.view )
addChildViewController( navController )
navController.didMoveToParentViewController( self )
YourRootController acts as the stacks's root (the bottom of the stack).
To push other controllers on top of the stack (your funnel controllers), simply use navController.pushViewController( yourControllerInstance!, animated: false ).
If you want to clear the stack after completion of the funnel, just call:
navController.popToRootViewControllerAnimated( false )
This function removes all views (besides the root controller) from your stack.

So I ended up having to do it programmatically by popping back to the first controller and then replacing from the funnel head to the dashboard head:
func bookingCompleteAcknowledged(){
//remove the popup controller
dismissViewControllerAnimated(true, completion: nil)
//remove current controller
dismissViewControllerAnimated(true, completion: nil)
if let topController = UIApplication.sharedApplication().keyWindow?.rootViewController {
if let navcontroller = topController.childViewControllers[0] as? UINavigationController{
navcontroller.popToRootViewControllerAnimated(false)
if let funnelcontroller = navcontroller.childViewControllers[0] as? FunnelController {
funnelcontroller.removeFromParentViewController();
funnelcontroller.view.removeFromSuperview();
let revealController = self.storyboard?.instantiateViewControllerWithIdentifier("DashboardController") as! SWRevealViewController
navcontroller.addChildViewController(revealController)
navcontroller.view.addSubview(revealController.view)
}
}
}
}

I believe you can do this by simply assigning a new array (with just the dashboard view, in your case) to the UINavigationController's viewControllers property.
Why do you want to use the same navigation controller instead of making a new one? Generally, instead of trying to change the root of a navigation controller, I would recommend just creating a new navigation controller.

Related

How to close multiple viewcontrollers in one time?

For my requirement, I have to go back to my rootviewcontroller (tabbar) but It have many page present on it.
example flow
my tabbar (have 4 tabs each tab has own navigation ) -> push(vc1) -> present nav(vc2) -> push vc3 -> present nav(vc4)
If I want to close all viewcontroller ( vc1 - vc4 ) How to dismiss them by one function ?
You can use UIViewController.dismiss(animated:completion:) call on the base view controller that started presentation from tab bar (root level).
If you present several view controllers in succession, thus building a stack of presented view controllers, calling this method on a view controller lower in the stack dismisses its immediate child view controller and all view controllers above that child on the stack. When this happens, only the top-most view is dismissed in an animated fashion; any intermediate view controllers are simply removed from the stack. The top-most view is dismissed using its modal transition style, which may differ from the styles used by other view controllers lower in the stack.
Given this hierarchy
my tabbar (have 4 tabs each tab has own navigation ) -> push(vc1) -> present nav(vc2) -> push vc3 -> present nav(vc4)
You can walk back the presentation hierarchy like following -
let vc4Presenter = vc4.navigationController?.presentingViewController
let vc2NavCtrl = (vc4Presenter as? UINavigationController) ?? vc4Presenter?.navigationController
let vc2Presenter = vc2NavCtrl?.presentingViewController
let vc1NavCtrl = (vc2Presenter as? UINavigationController) ?? vc2Presenter?.navigationController
vc2Presenter?.dismiss(animated: true, completion: {
vc1NavCtrl?.popToRootViewController(animated: false)
})
Above is merely an example of how you can find the correct view controller instance to call dismiss on in the view hierarchy. This is definitely not well suited for dynamic number of presentation layers.
You can -
Write this in a recursive way (so that it keeps looking for presentingViewController until it finds nil for the root level).
Have a convenient reference to tab bar controller throughout the app and call dismiss on it's currently selected view controller (tab).
If you want go directly to first viewController:
self.popToRootViewController(animated: true)
If you want to see how it is going away one by one
#objc func buttonPressed() {
let viewControllers: [UIViewController] = self.viewControllers
for aViewController in viewControllers {
self.popViewController(animated: true)
}
self.popToRootViewController(animated: true)
}

Close a viewcontroller after a segue

What I want is to close a viewController after performing a segue so that the back button of the navigation controller on the new view doesn't go back to the view that I just closed, but it goes to the view that precedes it in the storyboard like it is the first time that it is loaded.
I already tried stuff like dismiss and so but it doesn't really work for me as it only closes the view in which the button that I pressed for performing the function is located :
#objc func goToList(){
self.dismiss(animated: true, completion: nil)
performSegue(withIdentifier: "goToList", sender: nil)
}
The navigation controller maintains a stack (array) of ViewControllers that have been opened (pushed). It also has the ability to pop these ViewControllers off the stack until it gets to a specific one.
For example, if you wished to return to a previous view controller of type MyInitialVC then you'd want to search through the stack until you found that type of VC, and then pop to it:
let targetVC = navigationController?.viewControllers.first(where: {$0 is MyInitialVC})
if let targetVC = targetVC {
navigationController?.popToViewController(targetVC, animated: true)
}
NB. written from memory without XCode, so you may need to correct minor typos
You can use unwind segue to get back to each viewController that you want.
Read more here:
Unwind Segues Step-by-Step (and 4 Reasons to Use Them)

swift3 how to show navigation viewcontroller

I would like to open the specific view from the notification widget.
normally other answers are create new view..(let viewcontroller = ... )
so If you continue to call, continue use memory..
So, I want to open viewA, which is already opened in my app, and then move to viewB.
My application structure is basically a viewA when the application is launched.
What I think is that instead of creating a new viewA, I want to move to viewB using the navigation.push from the already existing viewA.
The way to check whether a particular page already exists, how to show that page at the top , and how to navigation.push is work.
now i'm using this code to open viewA from Appdelegate
let MainView: Main_View_List_ = mainStoryboard.instantiateViewController(withIdentifier: "Main_List_View") as! Main_View_List_
let nav = UINavigationontroller.init(rootViewController: MainView)
UIApplication.topViewController()?.present(nav, animated: true, completion: nil)
Assuming you have access to the navigation controller, you'll want to do something like this to pop back to viewA:
for viewController in nav.viewControllers {
if viewController is ViewAClass {
viewController.pushViewBFlag = true
nav.popToViewController(viewController, animated: true)
return
}
}
Once you get back to viewA, you can check for pushViewBFlag and create and push viewB.
Alternately, since it seems that you are setting viewA as the root of your navigation controller, you could do: nav.popToRootViewControllerAnimated(true) to get back to viewA. You would need to then handle pushing viewB from viewA.

How to push new view in detail view controller from master view controller?

I have been scratching my head trying to figure out how to do this. If anyone has some insight it would be greatly appreciated. I've attempted to do this using segues and push/presentViewController methods. With pushViewController nothing happens.
Scenario: Split view controller has two navigation controllers connected (one as master, one as detail). The master's navigation controller has a form with various cells that should control what is being displayed in the right hand side detail view when in landscape mode on the iPad. The navigation controller connected to the detail view has storyboard references connected to it (3 of them).
What I want to do: From the master view controller (which is the app menu), I would like to control what is being displayed in the detail view while maintaining navigation bar.
Attempt 1:
let detailVC = self.splitViewController!.viewControllers[1]
let newVC = UIStoryboard(name: "D", bundle: nil).instantiateViewControllerWithIdentifier("P")
detailVC.self.navigationController?.pushViewController(newVC, animated: true)
Attempt 2:
let detailVC = self.splitViewController!.viewControllers[1]
let newVC = UIStoryboard(name: "D", bundle: nil).instantiateViewControllerWithIdentifier("P")
detailVC.performSegueWithIdentifier("navP", sender: self)
One other related question I had...if a user does many hops between several of the menu options, how can one "reset" the back button's history in the navigation bar to prevent a case where clicking back will cycle you through the same several views?
You shouldn't be pushing the view controller or performing a segue but calling showViewController.
Do you definitely need to maintain the navigation bar or can you show different UINavigationBars (via showing your view controller embedded in an UINavigationViewController potentially)?
Alternatively just show a single view controller and use the logic you add in your view controller to change the content under the control of your master view controller.
After some experimenting with the best approach I solved this. :) Solution below for all devices (iPhone/iPad).
Define extension for UISplitViewController:
Modified version based off https://stackoverflow.com/users/4418308/santiago-bendavid
extension UISplitViewController {
func toggleMasterView() {
if UIScreen.mainScreen().bounds.height > UIScreen.mainScreen().bounds.width {
var nextDisplayMode: UISplitViewControllerDisplayMode
switch(self.preferredDisplayMode){
case .PrimaryHidden:
nextDisplayMode = .AllVisible
default:
nextDisplayMode = .PrimaryHidden
}
UIView.animateWithDuration(0.2) { () -> Void in
self.preferredDisplayMode = nextDisplayMode
}
} else {
// do nothing
}
}
}
Code in master navigation controller's root view controller:
let newVC = UIStoryboard(name: "some_storyboard_id", bundle: nil).instantiateInitialViewController()
if self.splitViewController!.viewControllers.count == 2 {
let detailVC = self.splitViewController!.viewControllers[self.splitViewController!.viewControllers.endIndex - 1]
if detailVC.childViewControllers[detailVC.childViewControllers.count - 1].restorationIdentifier! != "some_id_here" {
detailVC.childViewControllers[0].navigationController?.pushViewController(newVC!, animated: true)
}
self.splitViewController!.toggleMasterView()
self.navigationController?.splitViewController!.preferredDisplayMode = .Automatic
} else {
self.navigationController?.pushViewController(newVC!, animated: true)
}
This code works on regular iPhone (non-Plus) where the master view controller is the sole controller used for navigation. On iPads and iPhone 6/6s+ models (which behave same as an iPad) I check whether the current view is already present using the restoration ID property, and then present the new view if it's not the same as the one already rendered on screen and dismiss the master view controller if we're in portrait mode. If in landscape, we keep it on screen (default behavior).

How can I segue or pop back to the second scene of the first tab in a tabbed app, from a scene on another tab?

I have a storyboard set up with a tab bar controller and three tabs. Each tab has a navigation controller. The first tab has three scenes. There is a button (log out) in a view on the third tab that I would like to segue to the second scene on the first tab (corresponding to the log in view controller and connected to the first scene via Show(e.g., Push).
Here is what I've tried:
self.tabBarController?.selectedIndex = 0
This works, insofar as I get back to the first tab's initial scene after tapping the UIButton. But since I want to get to the second scene, this is not a complete solution. I think the solution may be along the lines of:
self.tabBarController?.selectedViewController = LoginViewController()
or
self.tabBarController?.setViewControllers(self.LoginViewController, animated: true)
But I do not want to create another instance of a view controller.
Can I still use .selectedIndex to implement a solution?
A simple solution u can try is
1. Set a Global variable (i.e in App Delegate) name as isLogoutClick of type boolean.
2. While you are on third tab and click on logout button then make the global variable "isLogoutClick" as true.
3.and then navigate to first tab (1st scene) and on viewDidLoad just check the condition that
if(appDelegate.isLogoutClick)
{
push your view to next scene.
}
4. make false the value of isLogoutClick.
5. make sure at initially the value of isLogoutClick is false.
try this might it will help you.
After setting selectedIndex to 0, perform the segue you want (in this example, "loginSegue"). You can name your segue in the storyboard if you haven't already.
tabBarController?.selectedIndex = 0
if let someViewController = tabBarController?.viewControllers?[0] as? SomeViewController {
someViewController.performSegueWithIdentifier("loginSegue", sender: nil)
}
I'm not sure if this works for tabBarController because I've used this for my navigationController but should work the same.
if let tab = self.tabBarController?.viewControllers {
if let index = find(tab.map { $0 is LoginViewController }, true) {
let destination = tab[index] as LoginViewController
tabBarController?.presentViewController(destination, animated: true, completion: nil)
}
}
With a navigationController I would use popToViewController but I'm not sure how the tabBarController exactly works

Resources