Sending data from my detail view back to my UITableView - iphone

First of all, thank God for Stack Overflow. I am new at Objective-C/Cocoa/iPhone development, and this site is an amazing aid.
I have a window, that has a tab bar, that has a navigation controller, that loads a UITableViewController. Clicking an "Add" button on the navigation bar (created programatically), pushes a UITableViewController like so:
InvoiceAddViewController *addController = [[InvoiceAddViewController alloc] initWithNibName:#"InvoiceAddViewController" bundle:[NSBundle mainBundle]];
[self.navigationController pushViewController:addController animated:YES];
This table view controller pushes it's own detail view:
UITableViewCell *targetCell = [self.tableView cellForRowAtIndexPath:indexPath];
GenericTextFieldDetailViewController *dvController = [[GenericTextFieldDetailViewController alloc] initWithNibName:#"GenericTextFieldDetailViewController" bundle:[NSBundle mainBundle]];
dvController.fieldName = targetCell.textLabel.text;
dvController.fieldValue = targetCell.detailTextLabel.text;
[self.navigationController pushViewController:dvController animated:YES];
[dvController release];
The concept being, you click on a cell in the table view controller such as "Notes". This pushes "GenericTextFieldDetailViewController" with the name of the "field" you clicked, and the value (if one already exists). This allows me to reuse my detail view rather than creating one ad nauseum for every field.
In order to push data back, I created a method on the "Add" UITableViewController:
- (void) updateField:(NSString*) fieldName value:(NSString*) fieldValue
{
UITableViewCell *targetCell;
if([fieldName isEqualToString:#"Invoice ID"])
{
NSUInteger indexArr[] = {1,1};
targetCell = [[self tableView] cellForRowAtIndexPath:[NSIndexPath indexPathWithIndexes:indexArr length:2]];
targetCell.detailTextLabel.text = fieldValue;
}
else if([fieldName isEqualToString:#"P.O. Number"])
{
NSUInteger indexArr[] = {1,2};
targetCell = [[self tableView] cellForRowAtIndexPath:[NSIndexPath indexPathWithIndexes:indexArr length:2]];
targetCell.detailTextLabel.text = fieldValue;
}
else if([fieldName isEqualToString:#"Add Note"])
{
NSUInteger indexArr[] = {3,0};
targetCell = [[self tableView] cellForRowAtIndexPath:[NSIndexPath indexPathWithIndexes:indexArr length:2]];
targetCell.detailTextLabel.text = fieldValue;
}
}
This method is designed to receive the data I push with this method in "Generic":
- (IBAction)saveField:(id)sender
{
self.fieldValue = theTextField.text;
InvoiceAddViewController *parentController = (InvoiceAddViewController*)self.view.superview;
[parentController updateField:self.fieldName value:self.fieldValue];
}
Which brings us to the problem:
When the save method fires off, it throws an invalid selector error because self.view.superview is not the UITableView that pushed the "Generic" detail view.
I have tried the following combinations (from GDB):
(gdb) po [[self view] superview]
<UIViewControllerWrapperView: 0x4b6d4d0; frame = (0 64; 320 367); autoresize = W+H; layer = <CALayer: 0x4b6c090>>
(gdb) po [[self navigationController] parentViewController]
<UITabBarController: 0x4d2fa90>
(gdb) po [self parentViewController]
<UINavigationController: 0x4d2fdc0>
I feel like I'm landing all around the UITableView I want to invoke, but can't find it.
What am I doing wrong?
refrains from pulling more hair out

The problem is that you are confusing the view hierarchy for the navigation stack. Your detail view controller wants to send a message to the controller that pushed it on the stack, which is the second to last object in the navigation controller's viewControllers array.
Try changing your saveField: method to:
- (IBAction)saveField:(id)sender
{
self.fieldValue = theTextField.text;
NSArray *navigationStack = self.navigationController.viewControllers;
InvoiceAddViewController *parentController = (InvoiceAddViewController*)[navigationStack objectAtIndex:navigationSTack.count - 2];
[parentController updateField:self.fieldName value:self.fieldValue];
}
Edit: I should note this design is very brittle. A better way is to apply Model-View-Controller and create an object that represents a field and title value. Then your InvoiceAddViewController can pass instances of these objects to your detail controller, and as your detail controller changes them, these changes can be easily reflected in your other controllers.
Edit 2: Here is a hint of how it will work.
UITableViewCell *targetCell = [self.tableView cellForRowAtIndexPath:indexPath];
GenericTextFieldDetailViewController *dvController = [[GenericTextFieldDetailViewController alloc] initWithNibName:#"GenericTextFieldDetailViewController" bundle:[NSBundle mainBundle]];
dvController.dataObject = [self dataObjectForIndexPath:indexPath];
[self.navigationController pushViewController:dvController animated:YES];
[dvController release];
I'm assuming you've implemented a dataObjectForIndexPath: method. Presumably tableView:cellForRowAtIndexPath: would also use this method to configure its cells.
Now, you can eliminate both the saveField: and updateField: methods. In your InvoiceAddViewController, viewWillAppear: could be used to refresh your view like this:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.tableView reloadData]
}
There is a whole world of possibilities here. reloadData is a very heavy handed one. Experiment with stuff like KVO to make this more automatic.
In your detail controller, of course, don't forget to update the object, say in view will disappear to do something like
self.dataObject.fieldValue = theTextField.text;
This is just to get you started. There are a lot of possibilities and details to consider. You should really look at a lot of sample code, this pattern gets used a lot. The CoreDataBooks example on the developer portal uses a similar pattern, there are almost certainly others.

You should be using notification, KVO or delegation. Use the design patterns that fit passing data around on this platform. Even if you can make the call directly using one of these patterns will most certainly prove to be a better way of achieving this in an encapsulated manner.

This approach is just screaming for delegation. Create a protocol on your detail view controller, declare a method like
- (void)genericTextFieldDetailViewController:(GenericTextFieldDetailViewController *)controller didUpdateValue:(NSString *)value forField:(NSString *)field
Now when you're ready to send the data back you would just send this message to your delegate (the pushing view controller).
[self.delegate genericTextFieldDetailViewController:self didUpdateValue:newValue forField:passedInField]
Make sure that when you create and push the detail view controller, you assign yourself as it's delegate and implement that method to handle the incoming values. I think you'll find this approach way more flexible.

I had the same problem, but not with a table view. I wanted to change a value from the last view controller. I'm using Xcode 5:
{
NSArray *navigationStack = self.navigationController.viewControllers;
ViewController *control = [navigationStack objectAtIndex:([navigationStack count] -2)];
control.pagecont.currentPage = self.currIndex;
CGRect frame;
frame.origin.x = control.scroll.frame.size.width * control.pagecont.currentPage;
frame.origin.y = 50;
frame.size = control.scroll.frame.size;
[control.scroll scrollRectToVisible:frame animated:YES];
[self.navigationController popViewControllerAnimated:YES];
}

Related

Return to the first index of UINavigationController?

I'm doing an application which uses a UINavigationController and I'm switching to other UIViewControllers as follows:
if(self.myViewController == nil){
MyViewController *aViewController = [[MyViewController alloc] initWithNibName:#"MyViewController" bundle:nil];
self.myViewController = aViewController;
[aViewController release];
}
AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
[delegate.myNavController pushViewController:myViewController animated:YES];
I imagine this is creating a pile of UIViewControllers into the UINavigationController, maybe an array of indexs? I would like to know how to turn back without having to be back one by one.
For example, I'm sailing through a few screens and with a button I would like to return at the first index of navigation. I would also like know how to modify indexes, view, erase and anything pertaining to this issue.
Sorry if I have not explained well.
You've asked two questions.
The first is, how do I get back to my first view controller. As #Thomas Clayson and #ender have answered, you want the popToRootViewControllerAnimated: method of your navigationcontroller object for that.
The second is how to move to a particular index in the view controller stack. The answer to that is, you can set the array of viewControllers explicitly. So you can pull out the current listing of view controllers, modify it, and set it back into the navigationController stack. It'll reset the stack and animate you moving to the top item in the stack.
Thusly:
NSMutableArray *controllers = self.navigationController.viewControllers;
[controllers removeObjectAtIndex:[controllers count] - 1]; //or whatever
[self.navigationController setViewControllers:controllers animated:YES];
NSArray *viewControllers = [[self navigationController] viewControllers];
for (int i = 0; i < [viewContrlls count]; i++){
id obj = [viewControllers objectAtIndex:i];
if ([obj isKindOfClass:[yourViewControllername class]]){
[[self navigationController] popToViewController:obj animated:YES];
return;
}
}
Using this you can come back to any specified viewController.
[self.navigationController popToRootViewControllerAnimated:YES];
Will take you back to the very first view controller (root view controller).
Hope this helps
Use this
NSArray *viewContrlls=[[NSArray alloc] initWithArray:[[self navigationController] viewControllers]];
id obj=[viewContrlls objectAtIndex:1];
[[self navigationController] popToViewController:obj animated:YES];
[viewContrlls release];
You should use popToRootViewControllerAnimated: From UINavigationController class reference:
Pops all the view controllers on the stack except the root view
controller and updates the display.
You can return to the first view with
[self.navigationController popToRootViewControllerAnimated:YES];
That being said, you can also remove a particular view controller, or navigate to a specific index in your view controller if you look at the example.
NSMutableArray *allViewControllers = [NSMutableArray arrayWithArray:navigationController.viewControllers];
// You can now manipulate this array with the methods used for NSMutableArray to find out / perform actions on the navigation stack
[allViewControllers removeObjectIdenticalTo: removedViewController];
// You can remove a specific view controller with this.
navigationController.viewControllers = allViewControllers;

iOS: Why isn't this View Controller getting pushed onto the Navigation Controller's stack?

I have a modal view that is a Navigation Controller. When one of the rows in its UITableView gets tapped, the correct View Controller for that row should be initialized and pushed onto the Navigation Controller's stack (so that the screen now shows that View Controller). But it's not working. I've been trying to debug it for a while, and it appears that the Navigation Controller's retain count is 0 at the time pushViewController is called. I assume that means it has been deallocated, and that this is the root of the problem. But I can't figure out why.
In the following code, AddSportDelegate.m presents the modal view that contains the necessary Navigation Controller (_addItemNavController) initialized with the necessary AddItemTableViewController. Tapping on one of the rows of the Table View managed by AddItemViewController calls the showAddItemDataView: method of AddSportDelegate, which in turn should push the correct ViewController onto the _addItemNavController stack. But, as I note in a comment in the code, the retain count of _addItemNavController at that moment is 0.
Note: I realize this code has memory leaks. I deleted some release lines for the sake of brevity. I also haven't included the code for the view controller that is supposed to be getting pushed, since it doesn't have anything at the moment beyond a UILabel identifying that it is the right View Controller.
AddItemDelegate.m
#synthesize addItemNavController = _addItemNavController;
- (void)showAddItemViewController:(UIViewController *)viewController
{
_parentVC = viewController;
[_parentVC retain];
tc = [[AddItemTableViewController alloc] init];
UIBarButtonItem *cancelButton = [[UIBarButtonItem alloc] initWithTitle:#"Cancel" style:UIBarButtonSystemItemDone target:self action:#selector(cancelAdd)];
tc.navigationItem.leftBarButtonItem = cancelButton;
tc.title = #"Select a Category";
_addItemNavController = [[AddItemNavController alloc] initWithRootViewController:tc];
tc.superViewController = _addItemNavController;
[_parentVC.navigationController presentModalViewController:_addItemNavController animated:YES];
}
- (void)showAddItemDataView:(SportCategory *)category
{
[category retain];
UIViewController *vc;
if (category.name == #"Soccer") {
vc = [[AddSoccerDataViewController alloc] init];
}else{
vc = [[AddBaseballDataViewController alloc] init];
}
//retain count already 0
NSLog(#"retain count: %i", [_addItemNavController retainCount]);
[_addItemNavController.navigationController pushViewController:vc animated:YES];
}
AddItemTableViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
_addItemDelegate = [[AddItemDelegate alloc] init];
SportCategory *soccer = [[SportCategory alloc] initWithCategoryName:#"Soccer"];
SportCategory *baseball = [[SportCategory alloc] initWithCategoryName:#"Baseball"];
_categories = [[NSArray alloc] initWithObjects:soccer,baseball,nil];
[self.tableView reloadData];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
SportCategory *selectedCategory = [_categories objectAtIndex:[indexPath row]];
[_addItemDelegate showAddItemDataView:selectedCategory];
}
I am going to take a shot at this.
if (category.name == #"Soccer")
I come from a java background, but I know a little objective - c. I thought you can't compare strings with == which would mean your view controller was never created. Maybe try a isEqualToString method.
That is my only thought, I could be wrong. But Best of Luck.
The '==' operator isn't the good way to compare strings, but anyway your code should fall into the else part.
About your question, _addItemNavController must be nil because your NSLog prints 0 for its retain count.
Is the method -(void)showAddItemViewController:(UIViewController *)viewController called somewhere ?
Your view controller doesn't seem to be initialized.
A bit of sleep helped me find the problem. There were actually two:
1) The final line in AddItemDelegate read:
[_addItemNavController.navigationController pushViewController:vc animated:YES];
However, _addItemNavController IS the navigation controller, so the '.navigationController' part needed to be deleted.
2) I also needed to assign tc.addItemDelegate to self in showAddItemViewController.

Strange UIKit bug, table view row stays selected

I'm facing what appears to be a UIKit bug, and it takes the combination of two less commonly used features to reproduce it, so please bear with me here.
I have quite the common view hierarchy:
UITabBarController -> UINavigationController -> UITableViewController
and the table view controller pushes another table view controller onto the navigation controller's stack when a row is selected. There's absolutely nothing special or fancy in the code here.
However, the second UITableViewController, the "detail view controller" if you will, does two things:
It sets hidesBottomBarWhenPushed to YES in its init method, so the tab bar is hidden when this controller is pushed:
- (id)initWithStyle:(UITableViewStyle)style {
if(self = [super initWithStyle:style]) {
self.hidesBottomBarWhenPushed = YES;
}
return self;
}
It calls setToolbarHidden:NO animated:YES and setToolbarHidden:YES animated:YES on self.navigationController in viewWillAppear: and viewWillDisappear: respectively, causing the UIToolbar provided by UINavigationController to be displayed and hidden with animations:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.navigationController setToolbarHidden:NO animated:YES];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.navigationController setToolbarHidden:YES animated:YES];
}
Now, if the second UITableViewController was pushed by selecting the row at the bottom of the screen (it doesn't have to be the last row) in the first controller, this row does not automatically get deselected when the user immediately or eventually returns to the first controller.
Further, the row cannot be deselected by calling deselectRowAtIndexPath:animated: on self.tableView in viewWillAppear: or viewDidAppear: in the first controller.
I'm guessing this is a bug in UITableViewController's drawing code which of course only draws visible rows, but unfortunately fails to determine correctly if the bottommost row will be visible in this case.
I failed to find anything on this on Google or OpenRadar, and was wondering if anyone else on SO had this problem or knew a solution/workaround.
I had this same exact problem, (though I am not using a toolbar). My solution was to deselect the row in didSelectRowAtIndexPath after pushing my second view controller.
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
RunViewController *runViewController = [[RunViewController alloc] initWithNibName:#"RunViewController" bundle:nil];
runViewController.managedObjectContext = self.managedObjectContext;
runViewController.hidesBottomBarWhenPushed = YES;
[self.navigationController pushViewController:runViewController animated:YES];
[runViewController release];
//deslect "stuck" row
[aTableView deselectRowAtIndexPath:indexPath animated:YES];
}
If I remember well I think I had same/similar problem once, but my solution was rather rough:
- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
// ...
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}

removing Views from SubView

I have a App that uses the In App feature.
-(void) completeTransaction: (skPaymenttransaction *)transaction{
}
When the above method gets called i want to remove all subviews and go back to my main menu window (the first view in my App).
Can anyone suggest to cleanest and best way to do this?
Cheers
EDIT:
Just to make things clear
Im not sure if this makes a difference but i have my main menu screen, then iam doing the following with an enter button.
UIViewController *controller = [[UIViewController alloc] initWithNibName:#"NibFile" bundle:nil];
controller.delegate = self;
controller.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentModalViewController:controller animated:YES];
[controller release];
Then i have a main screen with a button then when a user taps it, it then presents them with another modal view controller as above. On this view is a button that says BUY on it. They use clicks this and then the StoreKit does it business and once the payment is complete i want to get rid of the two modal controllers above and be left with the main menu screen.
Any Ideas.. ive tried as above
EDIT 2:
#Jordan Thanks,
But not sure if im doing this correctly. I understand the above code.
But when i start my app my app delegate loads a viewcontroller which is my main menu. Then i have a button that takes me to another view and on there is my features if the user clicks a feature that is not unlocked then it displays another view controller with the store on.
So with this in mind how do i get back to my main menu.
I have tried the following:
NSArray *subviews = [myAppdelegate.viewcontroller.view subviews];
for (int i=0; i<[subviews count]; i++)
{
[[subviews objectAtIndex:i] removeFromSuperview];
}
but i get and error along the lines of:
expected ':' before '.' ?
This should work.
// view is equal to your main view
NSArray *subviews = [view subviews];
for (int i=0; i<[subviews count]; i++)
{
[[subviews objectAtIndex:i] removeFromSuperview];
}
If you're talking about UIViewControllers and not subViews (they are different),then you can use:
[self.navigationController popToRootViewControllerAnimated:YES];
You're either adding UIViews to a UIViewController, in which case use my code above, or You're pushingViews (e.g. pushViewController) on top of a UIViewController, in which case use the code here.
We have to get the views to be removed in an array so that we can remove everything by means of enumeration
NSArray *ChildViews = [ParentView subviews];
for (UIView *childView in ChildViews) {
[childView removeFromSuperview];
}

How to pushviewcontroller to a viewcontroller stored in a tabbaritem?

First of all I know this is a long question. REST ASSURED I have tried to figure it out on my own (see: StackOverflow #2609318). This is driving me BATTY!
After trying and failing to implement my own EDIT feature in the standard moreNavigationController, I have decided to re-implement my own MORE feature.
I did the following:
Add a HOME view controller which I init with: initWithRootViewController
Add 3 other default tabs with:
ResortsListViewController *resortsListViewController;
resortsListViewController = [[ResortsListViewController alloc] initWithNibName:#"ResortsListView" bundle:nil];
resortsListViewController.title = [categoriesDictionary objectForKey:#"category_name"];
resortsListViewController.tabBarItem.image = [UIImage imageNamed:#"whatever.png"];
resortsListViewController.navigationItem.title=#"whatever title";
localNavigationController = [[UINavigationController alloc] initWithRootViewController:resortsListViewController];
localNavigationController.navigationBar.barStyle = UIBarStyleBlack;
[localControllersArray addObject:localNavigationController];
[localNavigationController release];
[resortsListViewController release];
Those work when i add them to the tabbar. (ie: click on them and it goes to the view controller)
Then I add my own MORE view controller to the tabbar:
MoreViewController *moreViewController;
moreViewController = [[MoreViewController alloc] initWithNibName:#"MoreView" bundle:nil];
moreViewController.title = #"More";
moreViewController.tabBarItem.image = [UIImage imageNamed:#"more.png"];
moreViewController.navigationItem.title=#"More Categories";
localNavigationController = [[UINavigationController alloc] initWithRootViewController:moreViewController];
localNavigationController.navigationBar.barStyle = UIBarStyleBlack;
[localControllersArray addObject:localNavigationController];
[localNavigationController release];
[moreViewController release];
Then
tabBarController.viewControllers = localControllersArray;
tabBarController.moreNavigationController.navigationBar.barStyle = UIBarStyleBlack;
tabBarController.customizableViewControllers = [NSArray arrayWithObjects:nil];
tabBarController.delegate = self;
That creates the necessary linkages. Okay, so far all is well. I get a HOME tab, 3 category tabs and a customized MORE tab -- which all work.
in the MORE tab view controller I implement a simple table view that displays all the other tabs I have in rows. SINCE I want to be able to switch them in and out of the tabbar I created them JUST like i did the resortslistviewcontroller above (ie: as view controllers in an array). When I pull them out to display the title in the tableview (so the user can go to that "view") i simply do the following:
// [myGizmoClass CategoryArray] holds the array of view controller tab bar items that are NOT shown on the main screen.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
... etc...
UIViewController *Uivc = [[myGizmoClass plusCategoryArray] objectAtIndex:indexPath.row];
cell.textLabel.text = [Uivc title];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
THIS is where it falls through:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
MyGizmoClass *myGizmoClass= [MyGizmoClass sharedManager];
UIViewController *tbi = [[myGizmoClass plusCategoryArray] objectAtIndex:indexPath.row];
NSLog(#"%#\n",[[tbi navigationItem ]title]);
[self.navigationController pushViewController:tbi animated:YES];
}
This is the error i get ("ATMs" is the title for the clicked tableview cell) so i know the Uivc title is pulling the correct title and therefore the correct "objectatindex":
2010-04-09 11:25:48.222
MouseAddict[47485:207] ATMs 2010-04-09
11:25:48.222 MouseAddict[47485:207]
*** Terminating app due to uncaught exception
'NSInvalidArgumentException', reason:
'Pushing a navigation controller is
not supported'
BIG QUESTION: How do i make the associated VIEW of the UIViewController *tbi show and get pushed into view?
I am GUESSING that the UIViewController is the correct class for this tbl .. i am not sure. BUT i just wanna get the view so i can push it onto the stack.
Can someone plz help?
To answer kovpas's question below: myGizmoClass is a singleton (apple's singleton myGizmo class. The array of viewcontrollers is stored in that just like it is in [localControllersArray addObject:localNavigationController]; (in the first code snippet above). AND it does put it in and pull it out correctly as evidenced by the fact that when i NSLOG the [Uivc title] the log prints ATMs. This means the plusCategoryArray is correctly storing and retrieving the viewController (if, indeed, that is what is being stored).
Pushing a navigation controller is not supported is really bothering me. Why would a viewController return a navigationController and is it possible to coerce the navigationController to get the "pushable" view out of it... or does the navigationController have some element in it that is the view?
From the error, it looks as if your Gizmo class has an array of UINavigationControllers, not UIViewControllers. So instead push with:
[self.navigationController pushViewController:[[tbi viewControllers] lastObject] animated:YES];
If the array is the same array as you called localControllers above, then this should work better. Or you could just create the array without the UINavigationControllers, they aren't needed if you are going to push them onto your more controller navigation controller.
I'm not sure, but it looks like this error appears when you are trying to push UINavigationController into another UINavigationController. Could you please provide an implementation of MyGizmoClass?