I have 4 view controllers I am dealing with in this question. For simplictiy sake I will refer to them as vcA, vcB, vcC, and vcD.
Overall what I want to do is dismiss two view controllers and then push the third view controller to the 4th view controller. Essentially this is how the path would go: vcA(dismissed) -> vcB(dismissed) -> vcC pushes to vcD(final destination).
Here is my current code for the process. Please note I am able to dismiss the first two view controllers but I can't figure out how to push the third view controller. Anyways here is my code:
CODE FOR vcA:
#IBAction func createGroupButtonTapped(_ sender: Any) {
if groupNameTextField.text == "" {
ProgressHUD.showError("Please enter a group title")
} else {
self.presentingViewController?.dismiss(animated: false, completion: nil)
NewConversationController().showDisplayConversationsController(title: self.groupNameTextField.text!, groupThumbnail: self.groupImageView, members: CreateConversationPopOver.groupMembers)
self.presentingViewController?.dismiss(animated: true, completion: nil)
}
}
In the code above I dismiss the first two view controllers and also call a function in vcB. This function in vcB calls a function in vcC. Here is the code for the functions:
FUNCTION IN vcB:
func showDisplayConversationsController(title: String, groupThumbnail: UIImageView, members: Array<String>) {
dismiss(animated: true, completion: nil)
DisplayConversationsController().showViewChatController(title: title, groupThumbnail: groupThumbnail, members: members)
}
FUNCTION IN vcC:
func showViewChatController(title: String, groupThumbnail: UIImageView, members: Array<String>) {
let viewChatController = ViewChatController(collectionViewLayout: UICollectionViewFlowLayout())
viewChatController.hidesBottomBarWhenPushed = true
viewChatController.groupThumbnail = groupThumbnail
viewChatController.groupMembers = members
viewChatController.groupTitle = title
navigationController?.pushViewController(viewChatController, animated: true)
}
The problem occurs in VCC where the view controller never pushes to the final destination otherwise known as vcD. I have added breakpoints within the function above and they are all called. So the code is being run.
I don't know why the view controller won't push because even after numerous testing, all my functions are being called.
Any help would be appreciated. Thanks!
PS: In case you are wondering why I want to dismiss views and present them this way, it is because I need to pass data through these views into a chat log controller.
first.
check vcC's navigationController, it may be nil.
It seems you call Class methods NewConversationController DisplayConversationsController, (not familiar with swift, or these two are ) not instance methods.
Maybe it not the right vcC, just a new vcC?
PS: It is quite strange that you vcA.presentingViewController dismiss twice and according to what you say, It just dismiss fine.
if you can describe how you present vcA, vcB or give a demo to show how it work, I think it may be helpful.
You can use Notification Pattern for the same.
If you want to switch directly from vcA to VcD, fire notification in vcA before switching to vcD. Make vcB, vcC as listener to the same notification. When u directly switch from vcA to vcD, vcB and vcC will get notified if they already present. Handle the notification in vcB and vcC and hence dismissViewController or PopViewController as required. You can even pass data through from vcA directly to vcD using notification.
You can't push a view controller into a view dismissed, if it's dismissed it disappears so it's not logical to push another view, cause the parent view controller is deleted.
What you have to do is - vcA --> Present vcB --> Dismiss vcB --> Present vcC Push vcD on vcC > Dismiss vcC
You can do it with notifications. You can even accomplish the task using delegation pattern. Make vcB as delegate of vcC that notifies vcB when vcB dismisses then dismiss vcB and then just push vcD on vcC.
Hope it helps. Happy Coding!!
Related
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)
I'm new with IOS and Swift so don't judge if solution is easy.
I have three ViewControllers like A,B and C.
I started from A -> NavigationController -> B -> NavigationController -> C
In specific situation I need to come back from C to A without seeing B. Is any way to do this?
Maybe changing the parent navigationController? Maybe I can print stack with every current view? - it will be really helpful.
I tried dismiss C and B view one by one and it work's but then we can see B view for a moment - so it's not a solution for me.
P.s : I'm using Modal kind to switch between controllers.
enter image description here
If A is always the first view controller, you can just do :
viewcontrollerC.navigationController?.popToRootViewController(animated: true)
This methods pop the stack to the first view controller, without displaying intermediates ones
If A is not the first viewController, you can do :
viewcontrollerC.navigationController?. popToViewController(viewControllerA, animated: true)
If you don't have a reference to viewControllerA, search it in the stack :
let viewControllerA: UIViewController?
for (let vc in (self.navigationController?.viewControllers ?? [])) {
//adust the test to find the appropriate controller
if vc.isKindOf(ViewControllerAClass.self) {
viewControllerA = vc
break
}
}
if let viewControllerA = viewControllerA {
self.navigationController?.popToViewController(viewControllerA, animated: true)
}
source : https://developer.apple.com/documentation/uikit/uinavigationcontroller/1621871-poptoviewcontroller
There are 2 ways you can achieve this. The simple to implement is in View Controller C you can, on in the specific situation, invoke following function:
navigationController?.popToRootViewController(animated: true)
This will pop all the navigational view hierarchy and take you back to the root i.e. the first view controller.
Second approach is to define unwind method in the view controller you want to go back to. In view controller when you start typing unwind, in Xcode 10 you will get autocomplete to add this Swift Unwind Segue Method.
#IBAction func unwindToA(_ unwindSegue: UIStoryboardSegue) {
let sourceViewController = unwindSegue.source
// Use data from the view controller which initiated the unwind segue
}
In this particular question let us say you added this method in View Controller A as you want to go back to it. I assume you have a button on View Controller C to go back to A. Controll+Drag from the button to the Exit symbol of the view controller A. The unwindToA method will automatically pop-up. Connect to it and you are done. When the user presses this button it will go back 2 navigation controllers to A.
Note: By this method you can go back to any navigation controller on the Navigation stack and it is not limited to root view controller alone. Below I am addition picture showing the exit on a view controller.
I want a my app can go to a first view controller when every time users want it.
So I want to create a function to dismiss all the view controllers, regardless of whether it is pushed in navigation controllers or presented modally or opened anything methods.
I tried various ways, but I failed to dismiss all the view controllers certainly.
Is there an easy way?
Try This :
self.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
it should dismiss all view controllers above the root view controller.
If that doesn't work than you can manually do that by running a while loop like this.
func dismissViewControllers() {
guard let vc = self.presentingViewController else { return }
while (vc.presentingViewController != nil) {
vc.dismiss(animated: true, completion: nil)
}
}
It would dismiss all viewControllers until it has a presentingController.
Edit : if you want to dismiss/pop pushed ViewControllers you can use
self.navigationController?.popToRootViewController(animated: true)
Hope it helps.
If you are using Navigation you can use first one
or if you are presenting modally you can second one:
For Navigation
self.navigationController?.popToRootViewController(animated: true)
For Presenting modally
self.view.window!.rootViewController?.dismissViewControllerAnimated(false, completion: nil)
Hello everyone here is the answer for Swift-4.
To go back to root view controller, you can simply call a line of code and your work will be done.
self.view.window?.rootViewController?.dismiss(animated: true, completion: nil)
And if you have the splash screen and after that the login screen and you want to go to login screen you can simply append presentedviewcontroller in the above code.
self.view.window?.rootViewController?.presentedViewController!.dismiss(animated: true, completion: nil)
Simply ask your rootViewController to dismiss any ViewController if presenting.
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.window?.rootViewController?.dismiss(animated: true, completion: nil)
(appDelegate.window?.rootViewController as? UINavigationController)?.popToRootViewController(animated: true)
}
The strategy to go back to your initial view controller could vary depending on your view controllers are stacked.
There could be multiple scenarios and depending on your situation, you can decide which approach is the best.
Scenario 1
Navigation controller is set as the root view controller
Navigation controller sets View Controller A as the root
Navigation controller pushes View Controller B
Navigation controller pushes View Controller C
This is a straightforward scenario where navigationController?.popToRootViewController(animated:true) is going to work from any view controller and return you back to View Controller A
Scenario 2
Navigation controller is set as the root view controller
Navigation controller sets View Controller A as the root
View Controller A presents View Controller B
View Controller B presents View Controller C
This scenario can be solved by the answers above
self?.view.window?.rootViewController.dismiss(animated: true) and will bring you back to View Controller A
Scenario 3
Navigation controller 1 is set as the root view controller
Navigation controller 1 sets View Controller A as the root
Navigation controller 1 pushes View Controller B
View Controller B presents Navigation Controller 2
Navigation Controller 2 sets View Controller D as the root
Navigation controller 2 pushes View Controller E
Now imagine that you need to go from View Controller E all the way back to A
Using the 2 answers above will not solve your problem this time as popping to root cannot happen if the navigation controller is not on the screen.
You might try to add timers and listeners for dismissing of view controllers and then popping which can work, I think there was an answer like this above with a function dismissPopAllViewViewControllers - I notice this leads to unusual behavior and with this warning Unbalanced calls to begin/end appearance transitions for
I believe what you can do to solve such scenarios is to
start by presenting your modal views controllers from the navigation controller itself
now you have better control to do what you want
So I would change the above to this architecture first:
Navigation controller 1 is set as the root view controller (same)
Navigation controller 1 sets View Controller A as the root (same)
Navigation controller 1 pushes View Controller B (same)
Navigation controller 1 presents Navigation Controller 2 (change)
Navigation Controller 2 sets View Controller D as the root (same)
Navigation controller 2 pushes View Controller E (same)
Now from View Controller E, if you add this:
let rootViewController = self?.view.window?.rootViewController as? UINavigationController
rootViewController?.setViewControllers([rootViewController!.viewControllers.first!],
animated: false)
rootViewController?.dismiss(animated: true, completion: nil)
you will be transported all the way back to View Controller A without any warnings
You can adjust this based on your requirements but this is the concept on how you can reset a complex view controller hierarchy.
Use this code for dismiss presented viewcontrollers and pop to navigation rootviewcontroller swift 4
// MARK:- Dismiss and Pop ViewControllers
func dismissPopAllViewViewControllers() {
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
appDelegate.window?.rootViewController?.dismiss(animated: true, completion: nil)
(appDelegate.window?.rootViewController as? UINavigationController)?.popToRootViewController(animated: true)
}
}
Swift 5.4:
self.navigationController?.popToRootViewController(animated: true)
Pops all the view controllers on the stack except the root view controller and updates the display.
func popToRootViewController(animated: Bool)
But if you want to go to specific controller just use the below function.
func popToViewController(UIViewController, animated: Bool)
Pops view controllers until the specified view controller is at the top of the navigation stack.
To achieve what you want, modify your navigation stack, then do popViewController.
let allControllers = NSMutableArray(array: navigationController!.viewControllers)
let vcCount = allControllers.count
for _ in 0 ..< vcCount - 2 {
allControllers.removeObject(at: 1)
}
// now, allControllers[0] is root VC, allControllers[1] is presently displayed VC. write back to nav stack
navigationController!.setViewControllers(allControllers as [AnyObject] as! [UIViewController], animated: false)
// then pop root VC
navigationController!.popViewController(animated: true)
See this for the way to further manipulate the navigation stack. If your topmost VC is modal, dismiss it first before the code above.
Create an Unwind Segue (You can find it at https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/UsingSegues.html copyright of Apple Inc.)
Unwind segues let you dismiss view controllers that have been
presented. You create unwind segues in Interface Builder by linking a
button or other suitable object to the Exit object of the current view
controller. When the user taps the button or interacts with the
appropriate object, UIKit searches the view controller hierarchy for
an object capable of handling the unwind segue. It then dismisses the
current view controller and any intermediate view controllers to
reveal the target of the unwind segue.
To create an unwind segue
Choose the view controller that should appear onscreen at the end of an unwind segue.
Define an unwind action method on the view controller you chose.
The Swift syntax for this method is as follows:
#IBAction func myUnwindAction(unwindSegue: UIStoryboardSegue)
The Objective-C syntax for this method is as follows:
- (IBAction)myUnwindAction:(UIStoryboardSegue*)unwindSegue
3. Navigate to the view controller that initiates the unwind action.
Control-click the button (or other object) that should initiate the unwind segue. This element should be in the view controller you want to dismiss.
Drag to the Exit object at the top of the view controller scene.
https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/Art/segue_unwind_linking_2x.png
Select your unwind action method from the relationship panel.
You must define an unwind action method in one of your view controllers before trying to create the corresponding unwind segue in Interface Builder. The presence of that method is required and tells Interface Builder that there is a valid target for the unwind segue.
In case anyone looking for an Objective-C implementation of the question's answer,
[self.view.window.rootViewController dismissViewControllerAnimated:true completion:nil];
func dismiss_all(view: UIView){
view.window!.rootViewController?.dismiss(animated: true, completion: nil)
}
May be what you are looking for is unwind segue.
Unwind segues give you a way to "unwind" the navigation stack back
through push, modal, popover, and other types of segues. You use
unwind segues to "go back" one or more steps in your navigation
hierarchy.
Link to documentation:
https://developer.apple.com/library/archive/technotes/tn2298/_index.html
The best and prefered way to do this is to create an unwind segue. Just follow this documentation https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/UsingSegues.html. It can de done in code or through the interface builder.
Does anybody know if there is a certain pattern for handling segues programmatically in a MVC way?
I would think the best way would be to work with an event system within a controller.
I want that all the view controllers connect to this navigationController instead of handling all the logic within the viewController logic itself. I want to out source this logic
In most of your view controllers, you will have access to a prepareForSegue function, with one parameter called sender.
If you kick off a segue programatically with performSegue(withIdentifier: "mySegueID", sender: yourVC) then this function will be called, and you'll be able to pass information from the sender to the new view controller.
In this function, to get a handle on the next VC, use segue.destinationViewController.
I don't know about a particular pattern but a simple way to programmatically handle transitions between 2 UIViewController could be to have a separated manager whose job is just to push/present/whatever new controllers over current, and to pop/dismiss/whatever current controllers to old ones.
The way I usually do this is by having a class we can name WorkflowManager, which will handle all transitions. Associated with this manager, you declare a WorkflowManagerComponent protocol and implement it :
protocol WorkflowManagerComponent {
var completionHandler: (hasCompleted:Bool,data:Any)->() {get set}
}
Make each UIViewController implement this, for example by calling completionHandler(true,someData) when the user taps a "next" button, or completionHandler(false,nil) when the user taps a "back" button.
Then in your workflow manager, you perform transitions to the next or previous UIViewController according to parameters sent in the completionHandler:
//init viewController1 ...
viewController1.completionHandler = onViewController1Completes
// ...
func onViewController1Completes(_ completed: Bool, data: Any) {
if hasCompleted {
//init viewController2 ...
viewController2.data = data
viewController2.completionHandler = onViewController2Completes
//Push the new vc
viewController1.navigationController.push(viewController2, animated: true)
} else {
//The vc1 was presented as a modal, dismiss it
viewController1.dismiss()
}
}
This way each UIViewController is separated from others, free off any transition logic.
A user is in a view controller which calls a modal. When self.dismissViewController is called on the modal, a function needs to be run on the initial view controller. This function also requires a variable passed from the modal.
This modal can be displayed from a number of view controllers, so the function cannot be directly called in a viewDidDisappear on the modal view.
How can this be accomplished in swift?
How about delegate?
Or you can make a ViewController like this:
typealias Action = (x: AnyObject) -> () // replace AnyObject to what you need
class ViewController: UIViewController {
func modalAction() -> Action {
return { [unowned self] x in
// the x is what you want to passed by the modal viewcontroller
// now you got it
}
}
}
And in modal:
class ModalViewController: UIViewController {
var callbackAction: Action?
override func viewDidDisappear(_ animated: Bool) {
let x = … // the x is what you pass to ViewController
callbackAction?(x)
}
}
Of course, when you show ModalViewController need to set callbackAction like this modal.callbackAction = modalAction() in ViewController
The answer supplied and chosen by the question asker (Michael Voccola) didn't work for me, so I wanted to supply another answer option. His answer didn't work for me because viewDidAppear does not appear to run when I dismiss the modal view.
I have a table and a modal VC that appears and takes some table input. I had no trouble sending the initial VC the modal's new variable info. However, I was having trouble getting the table to automatically run a tableView.reloadData function upon dismissing the modal view.
The answer that worked for me was in the comments above:
You likely want to do this using an unwind segue on the modal, that
way you can set up a function on the parent that gets called when it
unwinds. stackoverflow.com/questions/12561735/… – porglezomp Dec 15
'14 at 3:41
And if you're only unwinding one step (VC2 to VC1), you only need a snippet of the given answer:
Step 1: Insert method in VC1 code
When you perform an unwind segue, you need to specify an action, which
is an action method of the view controller you want to unwind to:
#IBAction func unwindToThisViewController(segue: UIStoryboardSegue) {
//Insert function to be run upon dismiss of VC2
}
Step 2: In storyboard, in the presented VC2, drag from the button to the exit icon and select "unwindToThisViewController"
After the action method has been added, you can define the unwind
segue in the storyboard by control-dragging to the Exit icon.
And that's it. Those two steps worked for me. Now when my modal view is dismissed, my table updates. Just figured I'd add this, in case anyone else's issue wasn't solved by the chosen answer.
I was able to achieve the desired result by setting a Global Variable as a boolean value from the modal view controller. The variable is initiated and made available from a struct in a separate class.
When the modal is dismissed, the viewDidAppear method on the initial view controller responds accordingly to the value of the global variable and, if needed, flips the value on the global variable.
I am not sure if this is the most efficient way from a performance perspective, but it works perfectly in my scenario.