I have three UIViewControllers (let's call them A, B and C) in a navigation controller. A can segue into either B or C. B can segue into C. When C closes, I want it to always return to A, i.e. it automatically closes B upon closing C, if opened from B.
Now, I tried using segue unwinding, so that when C closes, B's return method gets called to dismiss the destination controller:
- (IBAction)returnFromC:(UIStoryboardSegue*)segue
{
[segue.destinationViewController dismissViewControllerAnimated:YES completion:nil];
}
I put a breakpoint in this method - it is called. I have verified that the destinationController is indeed B. However, I noticed when the break point hits, C is still visible. After playing from the break point, C does exit as expected, but B is still visible.
Any ideas? Many thanks in advance.
When C closes, I want it to always return to A, i.e. it automatically closes B upon closing C, if opened from B
The simplest solution is: in C's viewDidAppear:, secretly remove B as a child of the navigation controller. In other words, you've got this:
A > B > C
Now you rearrange things so that you've got this:
A > C
Thus, the only thing to go back to from C is A.
It's easy to manipulate the navigation controller's set of children in this way. Just call setViewControllers:animated: (with a second argument of NO).
[But if I've understood your setup properly, another easy way would be to implement the unwind method in A and not B. Then do an unwind segue. We always unwind to the first view controller that contains the unwind method, so that would always be A.]
dismissViewControllerAnimated:completion: is used to dismiss a Modal segue and has no effect for Push segues
As #matt suggested you could just remove B (the middle) view controller from self.navigationController.viewControllers and here's a sample code that you can put in B view controller:
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// No need to process if the viewController is already being removed
if (!self.isMovingFromParentViewController) {
// Getting a mutable copy of the viewControllers (can't directly modify)
NSMutableArray *temp = [self.navigationController.viewControllers mutableCopy];
// Removing 'self' -> so A > B > C will become A > C
[temp removeObject:self];
// Setting the new array of viewControllers
self.navigationController.viewControllers = [temp copy]; // getting an immutable copy
}
}
P.S. You could use popViewControllerAnimated: instead of Unwind segue if you're not planning to send the data back.
Related
Let's say I have 3 controllers (A, B, C). A and C is ViewControllers, B is NavigationController. Normal application flow is A as root view, A present (modal) B, B push C.
What I want is to present C as top view controllers without going through all the animation from A-B-C but still have the hierarchy (means C can go back to A), is it possible?
We can set window rooViewController directly to C but it wont have the hierarchy
EDIT:
Maybe my question isnt clear enough, the main point here is, when I open my app, I want to show C directly but still have A->B->C view hierarchy so I can go back to A via normal pop and dismiss
EDIT2:
I manage to show C with B-C hierarchy, so I can pop back to B from C. Now my problem is how can I present B (NavigationController) from A (ViewController) so when I close B it will **dismiss* to A
EDIT3:
I saw some answer that use NavigationController, it works BUT not what I want because normally from A to B I use modal presentViewController and from B to A I use dismissViewController
EDIT4:
So far what I got is
self.window.rootViewController = vcA;
[self.window makeKeyAndVisible];
[vcA presentViewController:vcB animated:NO completion:nil];
[vcB pushViewController:vcC animated:NO];
this will give correct hierarchy that I want but it give fast animation (a blink) showing A and than C and also give warning Unbalanced calls to begin/end appearance transitions for <vcA: 0x7fcfa0cf9c50>.
EDIT5:
I endup ignoring the warning and stick with my prev answer (but still welcome for another solution). And for the blinking problem I use workaround below
uiview *overlay = [new uiview]; // using vcA.frame
overlay.backgroundColor = white; // I use dominant color of vcC
vcA addSubview:overlay;
self.window.rootViewController = vcA;
[self.window makeKeyAndVisible];
[vcA presentViewController:vcB animated:NO completion:^{
[overlay removeFromSuperview];
}];
[vcB pushViewController:vcC animated:NO];
This will disguise the blinking behavior so no one will notice (I hope :-p)
Use a UINavigationViewController and then call
setViewControllers(_:animated:)
Use this method to update or replace the current view controller stack without pushing or popping each controller explicitly. In
addition, this method lets you update the set of controllers without
animating the changes, which might be appropriate at launch time when
you want to return the navigation controller to a previous state.
If animations are enabled, this method decides which type of
transition to perform based on whether the last item in the items
array is already in the navigation stack. If the view controller is
currently in the stack, but is not the topmost item, this method uses
a pop transition; if it is the topmost item, no transition is
performed. If the view controller is not on the stack, this method
uses a push transition. Only one transition is performed, but when
that transition finishes, the entire contents of the stack are
replaced with the new view controllers. For example, if controllers A,
B, and C are on the stack and you set controllers D, A, and B, this
method uses a pop transition and the resulting stack contains the
controllers D, A, and B.
Let me know if it help you :)
push A and B and C like you normally would but do it by using presentViewController:? animated:NO and pushViewController:? animated:NO -- not animating is the clue
e.g. (mock code)
applicationDidFinishLaunching {
id a = [MyA new]; //root, presents b
id b = [MyA new]; //pushes c you said.. so it is or has a navigationController
id c = [MyA new];
[a presentViewController:b animated:NO];
b.navigationController pushViewController:c animated:NO];
}
To segue to any ViewController you want to: Have you drawn a custom segue in the storyboard by holding down the control key on your keyboard and clicking the ViewController you want the segue to be in? While still holding control, you can drag it to the viewcontroller you want to segue to. After that just let go and XCode will let you choose the type of segue you want: push, modal, or custom.
After that, click the visual segue reference that Xcode creates (looks like a big grey arrow in the storyboard that points to your viewcontrolelrs) and click the attributes inspector. Then look where it says identifier. From there you can name the segue anything you want and reference it programmatically. Just do the above and call the below message and you should be able to go to any viewcontroller when ever you want to.
[self performSegueWithIdentifier:#"ShowViewControllerA" sender:self];
Also, I agree with everyone else saying to set Viewcontroller C as root ViewController in the storyboard. PresentViewController is also a good idea. etc
Follow #Daij-Djan's answer:
applicationDidFinishLaunching {
id a = [MyA new]; //root, presents b
id b = [MyA new]; //pushes c you said.. so it is or has a navigationController
nav d = [[Nav alloc] initWithRoot:b];
id c = [MyA new];
[a presentViewController:d animated:NO];
b.navigationController pushViewController:c animated:NO];
}
Why don't you just present C on top of A with presentViewController?
EDIT:
A -> C:
In vcB I would add a boolean property indicating whether we are in the mentioned flow and present vbB in vcA this way:
// We are in vcA where you want to present vcB and vcC
vcB.transient = YES;
[vcA presentViewController:vcB animated:NO completion:^{
[vcB pushViewController:vcC animated:NO];
}];
C -> A
When you want to go back to vcA, popping vcC will call viewDidAppear in vcB.
// We are in vcB
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if(self.transient == YES) {
[self dismissViewControllerAnimated:NO completion:nil];
return;
}
}
With this solution when you go back from vcC to vcA you will temporary see vcB, as we have to wait for viewDidAppear to be called.
EDIT 2:
Alternatively if you don't need to directly go back from vcC to vbA, just use the first piece of code (no transient property required).
Keep A as rootVC(in applicationDidFinishLaunching). So when you open your app it will load A first.
Once A is loaded(in viewDidLoad) call a method to present B(keep animation = NO while presenting).
When B is loaded, call a method to push to C(keep animation = NO while pushing).
Hope this helps!
I have view A, view B and view C. I have pushed view B from view A and push view C from view B.
When user tap on back button in view C, I call popToRootViewControllerAnimated so that user won't see view B at all.
Problem is that if user swipe back in view C, they will still see view B. I don't want user to see view B at all and jump directly to view A. How shall I do?
I use xib currently.
You can change stack of NavigationController when ViewController C appears. Use below code:-
NSMutableArray *aMutArr = [NSMutableArray arrayWithArray:self.navigationController.viewControllers];
[aMutArr removeObjectAtIndex:aMutArr.count-2];
self.navigationController.viewControllers = aMutArr;
I have removed ViewController B from the stack. So if your user swipes back he will be able to see ViewController A.
you can turn off the swipe back feature
if ([self.navigationController respondsToSelector:#selector(interactivePopGestureRecognizer)]) {
self.navigationController.interactivePopGestureRecognizer.enabled = NO;
}
in that way the user can only go back by the button that you have provided
My storyboard structure is like:
A is a navigation controller
B is a tab bar controller
C,D,E are view controllers
F,G,H,I,J are a view controllers
if now i am on I ,and there's a button the i pressed then I go back to C.How to do that?
I tried make segue between I and C, but C has a back button, you pressed it,you back to I.
I don't want that.when i came from I to C, i want C is as I first come to C from B.
if i want to go to H from I,I want H have a back button that you pressed and you back to F not to I.
From I to C: do popViewController twice, or loop through the navigationcontroller's viewcontrollers and find C, pop to C.
From H to I: Programmatically push to I. In storyboard set an storyboard ID for I, and you can create a I instance through [storyboard instantiateViewControllerWithIdentifier:ID];
I would set the viewControllers property of UINavigationController.
Create an array of View Controllers like you would desire the stack to be. Example: #{C, F, I} or in the case of your question you would use #{ C } then update the navigationController to contain these views.
[self.navigationController setViewControllers:(NSArray *)];
If you want to do something other than what your Segues are set up for, be sure to give your VC a stoyboard ID
Then using that storyboard ID, you can call this code to create/display;
-(IBAction)myButtonAction:(id)sender{
RDLaunchOptionsTableViewController *vc = [self.storyboard instantiateViewControllerWithIdentifier:#"RDLaunchOptionsTableViewController"];
vc.delegate = self;
[self.navigationController pushViewController:vc animated:YES];
}
You don't have to use a nav controller. Just use [self presentViewController....];
I have a instance of UINavigationController namely N declared for my UIPopoverController in a method. I have two UIViewController namely A and B. Initially when I load the popover I assign viewcontroller A to my navigation controller N so A's view get displayed. At this point when N displays A, N has done button of type UIBarbuttonItem assigned as rightNavigation item and calls a method namely M().
So Here is the Question- When I press Done I need to load view controller B in the called Method M(). That is push B in N but for doing that I need the instance of navigation controller N from the UIBarButtonItem that I pressed. I assumed some thing like
-(void)M:(id)sender
{
UINavigationController *N = barButton.parentController;
[N pushViewController:B animated:NO];
}
But I didnt arrive to any solution. Can someone please help me with this. Thank you.
In method B, you use :
[self.navigationController pushViewController:ac animated:YES];
//ac == UIVIewController which you want to push
I have the following view controllers built in my app. The presenting controller is "PatientSelectViewController" (let's call it controller A) and it allows to either manually enter the patient ID in a text field or press a "Scan Barcode" button which would perform a segue to another view controller - namely, "BarcodeScanViewController" (let's call it controller B).
When B finishes scanning a barcode and returns a result (a patient ID), I notify the presenting view controller (A) about it and A is responsible for looking up the ID in a database. At this point the controller B should be dismissed. If the ID is found, then we transition to the third view controller - "PatientConfirmViewController" (let's call it C). However, if the ID is not found then I want a pop up message that says so and go again to the controller B to scan the barcode again.
Similarly, if the user decided to manually enter the ID in the text field instead of scanning it, then a successful ID would take me to the controller C while an unsuccessful one would give a pop up message and remain in controller A for another try.
I also want the controllers to be embedded in a navigation controller, so that I always have tabbar buttons that take me back to the previous view controller. For example, I will have a tabbar button to return to A from either B or C. Ideally, if I reach C after a successful barcode scan, I'd like the tabbar button to take me back to B - not A! - in case the user decides that she doesn't want to confirm this ID, the idea being that the user would likely want to rescan a barcode. But this is not critical.
I am having trouble implementing this for some reason. Here is an example of a screwed up behavior: I am calling A then calling B (to scan a barcode) and scan a barcode that I know is in the database. This correctly brings me to C with the patient info displayed. But then I decide to go back to A using the tabbar button "Enter Patient ID" Then I press the "Scan barcode" button again, again scan the same barcode as before but this time instead of a successful transition to C, I am getting this screen - note the screwed up tabbar! It must be saying "Confirm ID" and "Enter Patient ID" at the same time and the buttons go back to Login (this is the controller that invoked A in the first place) and "Scan Barcode" - that is, the controller B as if it were never popped up previously!
This can happen randomly after 2 or 3 or more successful scans. The log displays this:
nested push animation can result in corrupted navigation bar
Unbalanced calls to begin/end appearance transitions for
.
Finishing up a navigation
transition in an unexpected state. Navigation Bar subview tree might
get corrupted.
Here is how I implemented it:
In view controller A:
-(void)prepareForSegue: (UIStoryboardSegue *)segue sender: (id)sender
{
if ([[segue identifier] isEqualToString:#"BarcodeScanView"])
{
self.p_usingBarcodeScan=YES;
[[segue destinationViewController]setViewDelegate:self]; //sets itself as a delegate for receiving the result of a barcode scan from controller B
}
if ([[segue identifier] isEqualToString:#"ConfirmID"])
{
[[segue destinationViewController] setP_userInfo:p_userInfo] ; //passes the data to the controller C
}
}
The delegate method for receiving a barcode scan result (still in controller A):
- (void) didScanBarcode:(NSString *)result
{
self.p_userID = result;
[self.navigationController popViewControllerAnimated:YES];//Pop B from the navigation stack to return to A - is this right????
//Run the database query
[self lookUpID];
}
The method that looks up the ID in the database (still in A):
- (void) lookUpID{
/*.....
Does something here and gets a result of the lookup...
*/
// Do something with the result
if ([[result p_userName] length] > 0 ){ //Found the user!
p_userInfo = result;
[self performSegueWithIdentifier: #"ConfirmID" sender: self];
}
else {
UIAlertView * messageDlg = [[UIAlertView alloc] initWithTitle:nil message:#"User was not found. Please try again"
delegate:self cancelButtonTitle:nil otherButtonTitles:#"OK", nil];
[messageDlg show];
//Here I'd like to perform this seque to B only if I got here after a barcode scan...
//Otherwise I am just staying in A...
if (self.p_usingBarcodeScan == YES ){
[self performSegueWithIdentifier: #"BarcodeScanView" sender: self];
}
}
return;
}
Just for completeness, in B once I managed to scan a barcode, I am calling this:
- (void)decodeResultNotification: (NSNotification *)notification {
if ([notification.object isKindOfClass:[DecoderResult class]])
{
DecoderResult *obj = (DecoderResult*)notification.object;
if (obj.succeeded)
{
decodeResult = [[NSString alloc] initWithString:obj.result];
[[self viewDelegate] didScanBarcode:decodeResult];
}
}
}
I am using push seques from A to B and from A to C and using storyboards.
Here is a snapshot of the storyboard, with the segues from A to B ("BarcodeScan") and A to C ("ConfirmID") visible. Both are push segues:
Thanks a lot in advance!
You don't say whether you are currently using a navigation controller and push segues, or presenting with modal segues.
Here:
[self.navigationController popViewControllerAnimated:YES];//Pop B from the navigation stack to return to A - is this right????
[self dismissViewControllerAnimated:YES completion:nil];**//is this right???**
The first is correct for returning from a push segue, the second is appropriate for a modal/presenting segue. The push-return method is effectively what happens when you use the back button in a navigation controller.
update
I think you need to untangle your navigation methods a little. What I suggest
In B, have a delegate method that
checks the patient ID
if it's good sets self.p_userID in A
returns a BOOL success/fail back to B.
_
based on that result, either :
pop yourself off (you can use [self.navigationController popViewController] directly in B) or
bring up your alert in B. Given that you have a back button in B, and (perhaps) a rescan button, your alert may not need to present any choices.
In A :
- (void) viewWillAppear:(BOOL)animated
{
NSLog (#"viewControllers %#",self.navigationController.viewControllers);
[super viewWillAppear:animated];
if (self.p_userID) {
[self performSegueWithIdentifier: #"ConfirmID" sender: self];
self.p_userID = nil;
}
}
(this performSegue should only happen if you set self.p_userID while you were still in B)
The typed-in userID logic is simpler. Again you check the patient id. If it is not there, throw up an alert in A (again, you shouldn't need to present choices, as all the nav options are available without the alert). If it is there, set self.p_userID to the ID and initiate the segue.
In prepareForSegue you should do your lookup to get the userInfo dictionary from self.p_userID to pass to C, then set self.p_userID to nil. Alternatively (better) just pass self.p_userID to C and do the lookup in C (assumes you have a separate model source object). Whatever you do, be sure to set self.p_userID to nil whenever you leave A so that you don't auto-trigger a segue you don't want! Perhaps zero it in 'viewWillDisappear' as well.
OK, I am trying to partially answer my own question.
Even after implementing He Was suggestion above, my troubles persisted and even multiplied (some details on these are in my comment in the discussion thread https://chat.stackoverflow.com/rooms/23918/discussion-between-peterd-and-he-was)
However, by some change I googled the log message I was getting: "nested push animation can result in corrupted navigation bar" and wound up reading this answer: https://stackoverflow.com/a/5616935/1959008, which suggested that my issue was using
[self.navigationController popViewControllerAnimated:YES];
that is with animated set to YES. Once I set it to NO, the issues with the tabbar disappeared (some small quirks remain and I hope to solve them soon). This is really strange - looks more like a bug than a feature to me, but I could be wrong of course...