Trouble coordinating view controllers - ios

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

Related

Switch to a view controller in a different tab after user performs action

I have the following storyboard in an application I am working on:
At the root, I have a Tab Bar Controller. It links to two View Controllers.
The first View Controller to display a newsfeed with pictures uploaded by the user (the one at the bottom in the storyboard).
The second View Controller serves to initiate the taking of a picture and attach some data to it. In the last step (top right), when touching "Save" in the right item of the Navigation bar, I want the user to be redirected to the newsfeed View Controller passing it some data.
I tried using a segue and it works. The data are passed to the newsfeed but the wrong tab is selected. I changed the selected tab using
[self.tabBarController setSelectedIndex:0];
But by tapping on the second tab again, things are messed up. I can see the newsfeed instead of the taking a picture screen. If I tap again, it crashes.
At some point I thought I may have got the wrong storyboard and should have implemented a TabBar in my newsfeed and handle the taking picture as a modal view.
Would you know any clean way to achieve this?
Thanks
You should not use a normal segue, which adds the destination controller to the stack. To do what you are trying to the best way should be to use an unwind segue. This is a rough sketch of what you need to do:
• Declare an unwind segue action in the NewsfeedController like (IBAction)unwindFromPictureSaved:(UIStoryboardSegue *)segue;
• Connect your "Save" button in your SavingPictureController to the "Exit" icon in the storyboard and select the previously defined method;
• In the newly created unwind segue define its identifier with something like SavedPictureSegue;
• Define the data to be passed in SavingPictureController's header with something like #property (strong, readonly, nonatomic) id passedData;
• In SavingPictureController implement
-(void)prepareForSegue:(UIStoryboardSegue *)segue
{
if ([segue.identifier isEqualToString:#"SavedPictureSegue"]) {
_passedData = // Your data here
}
}
• In NewsfeedController now implement the previously defined method and fetch the data from (SavingPictureController *)segue.sourceController. Be sure to #import "SavingPictureController.h".
Thanks to #Davide, I created a subclass of TabBarController and implemented the method below:
// Find the appropriate controller to answer to an unwind segue
// For each child view controller
// Checks if it is a Navigation Controller
// If it is check its children view controllers
// Return the first view controller that answers the unwind segue
// This because I assumed the default behavior is just to check one level up (in this case, it would have stopped at the NavigationController)
// Based on https://developer.apple.com/library/ios/technotes/tn2298/_index.html#//apple_ref/doc/uid/DTS40013591-CH1-CCVC-SELECTING_A_CHILD_VIEW_CONTROLLER_TO_HANDLE_AN_UNWIND_ACTION
- (UIViewController *)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender {
BOOL resChildren, res;
for(UIViewController *controller in self.childViewControllers) {
if ([controller isKindOfClass:[UINavigationController class]]) {
for (UIViewController *childController in controller.childViewControllers) {
resChildren = [childController canPerformUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
if (resChildren) {
return childController;
}
}
}
res = [controller canPerformUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
if (res) {
return controller;
}
}
return nil;
}
Then in the unwind method of the 'NewsFeedController" it is necessary to set the correct index to see the controller with something like:
[self.tabBarController setSelectedIndex:1];
I uploaded a demo on github at https://github.com/kintso/unwindSegueWithTabBarControllerAndNavigationController

How can i hold the value of a string when the viewController goes to another tableViewController

Well, that question sure sounds weird but i couldn't find a better way to put it.
I m pretty sure its a basic mistake but i m stuck.
I got a main home view controller, there are 2 buttons which leads to 2 different tableViewController.
i will use both of the selections.
But when i get the selected index from one table view and go the the next one, the first one's value become null.
if (tempFromLocationString!=NULL) {
//tempFromLocationString=#"asd";
fromLocationLabel.text=tempFromLocationString;
}
if (tempToLocationString!=NULL) {
toLocationLabel.text=tempToLocationString;
}
this is how i segue from tableView to View controller
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"fromLocationSegue"])
{
NSLog(#"%#",selectionString);
ViewController *vc = [segue destinationViewController];
vc.tempFromLocationString=selectionString;
}
}
and this is how i get the selected cell's value.
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
selectionString=[fromLocationArray objectAtIndex:indexPath.row];
NSLog(#"%#",selectionString);
}
this is my code. i get temp strings with segues and i m applying these codes in view did load.
all the NSStrings declared in .h files.
the flow is like this;
user enter the app,
select a button,
goes to the first table view controller
select a location,
clicks ok button and goes back to the first view controller with segue ( selectionString)
the label is set to the selectionString appropriately
user click next button,
goes to the select table view
select a location
clicks ok and goes back the first view controller now the second label is set to the selectionString appropriately but now the first one is deleted and the string become null
OK
Your app flow
Case1
User enter the app - Correct
Select a button - Correct
Goes to the First TableViewController select a location -
Correct
Clicks ok button - Correct
and Goes back to the first view controller with segue
(selectionString) the label is set to the selectionString
appropriately - Incorrect
Step 5 is incorrect, why?
Answer - Because you are again pushing the ViewController after the selection in tableViewController, where as your ViewController already exist in the stack, so here instead of using segue, you should just pop the viewcontroller with same reference taken from ViewController.
Case2
User click next button - Correct
Goes to the select table view select a location clicks ok - Correct
and goes back the first view controller now the second label is set to the selectionString appropriately but now the first one is deleted and the string become null - Incorrect
Step 3 is incorrect the same way as Case1.
Answer- Again you are actually not going back, you are going forward, so what happens is you are creating a new instance of ViewController on selection, which doesn't have the previous selected value.
Solution
Create NSString property in each respective tableViewController separately same as you have in ViewController.
When you segue tableViewController from ViewController, assign the property like
TableViewController *vc = [segue destinationViewController];
vc.tempFromLocationString=self.tempFromLocationString;
On selection in tableviewcontroller do the following
self.tempFromLocationString=selectionString;
[self.navigationController popViewController:YES];
Now instead of assigning value in ViewDidLoad in ViewController, do it in ViewWillAppear.
I hope it helps.
Maybe your strings are not NULL when you set your labels.
Try to put a breakpoint before those lines, and check your temp strings
if (tempFromLocationString!=NULL) {
//tempFromLocationString=#"asd";
fromLocationLabel.text=tempFromLocationString;
}
if (tempToLocationString!=NULL) {
toLocationLabel.text=tempToLocationString;
}
If they are not NULL try this:
if (tempFromLocationString && [tempFromLocationString length] > 0) {
fromLocationLabel.text=tempFromLocationString;
}

Multiple views to one view, how do I go back to the sender

I am connecting three different views to one view and I am trying to create a back button that goes back to the view that I came from.
Example:
View A, B and C are connected to view D. I want to create a back button that goes back to say B, if I went to D from B. If I went to D from C, I want that button to go back to C and so on. How do I do this programmatically?
Here's some code:
On the sender we are using
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"SegueA"]) {
ViewController *destViewController = segue.destinationViewController;
destViewController.segueName = #"SegueA";
}
}
On the receiver side, we are using the following code :
- (IBAction)backBtn:(id)sender {
if([_segueName isEqualToString: #"SegueA"]){
[self performSegueWithIdentifier: #"SegueAA" sender:self];
}
So we are using a segue to go from A to D and then, if SegueA is identified we want to return via a segue from D to A called SegueAA.
Assuming you have UIViewController instead of UIView, you can use
[self.navigationController popViewControllerAnimated:YES]
You need to make a manual segue. You do this by making a segue from one view to another and then giving it a name in the interface builder.
Control Drag from one view to another
Under the "manual" segue type you probably want to use push
Click on the segue and go to properties and give it a name under the "Identifier" field
Then you can call it like so:
[self performSegueWithIdentifier:#"initalLegalSegue" sender:self];
(this block for example would launch my "initialLegalSegue" manually by code.)
With modal segues, I've been using this:
This goes back one view controller:
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
This goes back two view controllers:
[self.presentingViewController.presentingViewController dismissViewControllerAnimated:YES completion:nil];

unwind segue / performSegueWithIdentifier: problems

in my app, there is a chatting service. when the user wants to message someone new, he press on "new chat" and a Table View Controller is shown to select from the list of friends. i'm using unwind segues in order to go back to the previous view controller and share the data between the two view controllers (mainly the data will be the friend to have a chat with). the unwind segue works perfectly, however in my app, when the user goes back to the main VC, i fire a new segue in order to go to another VC directly where the user has the chat; and this isn't working. All segues are well connected and i tried NsLog in every corner and it's entering there and even the prepareForSegue: is being accessed. i tried putting an NsTimer, thought there might be some technical conflict, and it didn't work. i change the segue to the chat VC to a modal and now it's giving me this error:
Warning: Attempt to present on whose view is not in the window hierarchy!
i can access this VC in other ways and it's in the hierarchy. my questions are: what could be wrong? does unwind segues alter the windows hierarchies ?
PICTURE:
to present the problem more visually, the main VC i'm talking about is on the bottom left connected to a navigation view controller. when a user presses new chat, the two VC on the top right are presented (one to choose between friends or group, the other to show the friends/ groups). so when a friend is selected for say from the top right VC i should unwind segue to the main VC. as you can see from the main VC there is other segues. non of them can work if i do unwind segue and they do work if i operate normally.
The reason it is not working and is giving you that error is because things arent happening in the order you think they are.
When the unwind happens the view which is visible is not dismissed yet, therefore you are trying to perform a segue on a view which is in fact not in that hierarchy like the error says, take this for example, placing NSLog statements in the final view and then in the unwind method in your main view controller you can see the following:
2013-11-27 14:51:10.848 testUnwindProj[2216:70b] Unwind
2013-11-27 14:51:10.849 testUnwindProj[2216:70b] Will Appear
2013-11-27 14:51:11.361 testUnwindProj[2216:70b] View Did Disappear
Thus the unwind in the main view is getting called, the view will appear (your main view controller), and then your visible view is dismissed. This could be a simple fix:
#interface ViewController () {
BOOL _unwindExecuted;
}
#end
#implementation ViewController
- (void)viewWillAppear:(BOOL)animated
{
NSLog(#"Will Appear");
if (_unwindExecuted) {
_unwindExecuted = NO;
[self performSegueWithIdentifier:#"afterunwind" sender:self];
}
}
- (IBAction)unwind:(UIStoryboardSegue *)segue
{
NSLog(#"Unwind");
_unwindExecuted = YES;
}
Don't use timers or delays to try and anticipate when a view may exist.
Instead, use calls like: - (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion
The completion block will let you know when you've arrived back at the main VC. Alternatively, look at the various calls associated with segues so that you know precisely when you can perform operations on the new window.
If all else fails, there's always UIViewController viewDidAppear.
This is a common problem for the view controller that is handling the unwinding, because during unwind, that view controller is likely to not be in the window hierarchy.
To solve, I added a property segueIdentifierToUnwindTo to coordinate the unwinding.
This is similar to the answer by JAManfredi, but extending it to be able to segue to any view controllers.
#interface FirstViewController ()
// Use this to coordinate unwind
#property (nonatomic, strong) NSString *segueIdentifierToUnwindTo;
#end
#implementation FirstViewController
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Handle unwind
if (_segueIdentifierToUnwindTo) {
[self performSegueWithIdentifier:_segueIdentifierToUnwindTo sender:self];
self.segueIdentifierToUnwindTo = nil; // reset
return;
}
// Any other code..
}
// Example of an unwind segue "gotoLogin"
- (IBAction)gotoLogin:(UIStoryboardSegue*)sender {
// Don't perform segue directly here, because in unwind, this view is not in window hierarchy yet!
// [self performSegueWithIdentifier:#"Login" sender:self];
self.segueIdentifierToUnwindTo = #"Login";
}
#end
I also shared how I use unwind segues with a FirstViewController on my blog: http://samwize.com/2015/04/23/guide-to-using-unwind-segues/
okay solved this, the problem is that the view wasn't there yet and i had to delay the process by this:
[self performSelector:#selector(fireNewConv) withObject:nil afterDelay:1];
However, something interesting is that i tried delaying by NSTimer but it didn't work.

IOS: Stay on the first view controller before complete all controls (StoryBoard)

I connected first view controller with the second one using StoryBoard Push Segue and Interface Builder.
The button is named GO on top/right.
I have three textfield that must be filled before going to second controller.
I display an alert when one of them is empty.
The problem is that my code after displaying correct alertView goes to SecondController instead of remaining on mainController.
if ([segue.identifier isEqualToString:#"DataDisplay"])
{
if (![self verifySelection]) {
return;
} else {
RowViewController *rowViewController = segue.destinationViewController;
// rowViewController.delegate = self;
}
}
1) You have a segue wired directly from your Go button to your Sensor Data view controller. You don't want this, because anytime someone touches Go, the segue is going to happen ... no stopping it. So, first step is to remove the segue you have going from Go to your second view controller.
2) Instead, wire the segue from the File's Owner icon below the view controller to the second view controller. Give it a name like DataDisplay.
3) In the IBAction for your Go button
if ([self verifySelection) {
[self performSegueWithIdentifier:#"DataDisplay" sender:self]
}
An easy fix would be to create the segue manually, rather than letting the interface builder manage it. So you would ctrl-drag from your main view controller to your second one, selecting push as the type of segue and assigning it an identifier through the identifier inspector, then you connect an IBAction to your Go button and in the method you perform the checks on the text fields before programmatically firing the segue with:
[self performSegueWithIdentifier:#"whateverIdentifierYouGaveYourSegue" sender:self];
Heads up: to create a manual segue from a viewcontroller to another one, you need to either zoom out in your storyboard or ctrl-drag from the yellow circle underneath the view!
Edit: Your IBAction connected to the button method should be something like the following:
- (IBAction)download:(id)sender {
if(text boxes are ok)
[self performSegueWithIdentifier:#"segueIdentifier" sender:self];
else
[self showWarning];
}
Make sure that you assigned the ID segueIdentifier to the segue you created in your storyboard.
Your problem is you are defining the "performSegueWithIdentifier" after displaying the alert.
I think the code you are doing is like this :
//AlertView Allocation
[alert show];
Perform Segue
If this is how you are doing, then you are doing it wrong.
You have to use the structure of If-Else Statements and put up the Perform Segue in the condition where all the textfields are filled.

Resources