Delegation and dismissal of a view controller - iphone

I'm doing an app that has a UITableViewController populated with a list of products. Each row segues to a UIViewController that presents the details of the product in the row. Now since tapping on each row and going back to see the details of the next product might be too tedious for the user, we decide to add this feature: when a user swipes on the UIViewController of a product, then the UIViewController with the details for the next product is pushed.
But, as of now, I'm not sure of the best way to implement this. I'm tempted to pass the array of products to the UIViewController so that the swiping is achieved but this will be a violation of the MVC framework,right? Views cannot own the data they're presenting. The product details UIViewController should only know about the specific product that's passed to it, not the rest, right?
I think this can be accomplished using delegation but I'm not sure how. Can anybody help me? Thanks!
EDIT:
Rob Mayoff's code was really helpful so I decided to implement it. But for the meantime, instead of implementing a swipe, I'll just use a simple round rect button to call the functions.
- (IBAction)showNextProduct:(id)sender {
[self.productsTVC goToProductAtIndex:self.productIndex + 1];
}
- (IBAction)showPriorProduct:(id)sender {
[self.productsTVC goToProductAtIndex:self.productIndex - 1];
}
But every time I click any of the buttons, my app crashes with the message: Finishing up a navigation transition in an unexpected state. Navigation Bar subview tree might get corrupted. Unbalanced calls to begin/end appearance transitions for <ProductDetailsViewController: 0x6e510c0>.

Let's say you have a CatalogViewController (which is a subclass of UITableViewController) and a ProductViewController (which is a subclass of UIViewController).
The simplest way to implement “swipe to next product” is to give the ProductViewController a reference to the CatalogViewController. It should be weak (if using ARC) or assign (if not using ARC). You will also want a property that holds the index of the product in the catalog:
#interface ProductViewController
#property (nonatomic, weak) CatalogViewController *catalogViewController;
#property (nonatomic) NSInteger productIndex;
#end
Then in the action method for a swipe, you send a message to the CatalogViewController asking it to go to the next (or prior) product in the catalog:
#implementation ProductViewController
- (IBAction)showNextProduct:(id)sender {
[self.catalogViewController goToProductAtIndex:self.productIndex + 1];
}
- (IBAction)showPriorProduct:(id)sender {
[self.catalogViewController goToProductAtIndex:self.productIndex - 1];
}
In CatalogViewController, whenever you create a ProductViewController, you need to set those properties:
#implementation CatalogViewController
- (ProductViewController *)productViewControllerForProductAtIndex:(NSInteger)index {
if (index < 0 || index >= self.products.count)
return nil;
ProductViewController *vc = [[ProductViewController alloc] initWithProduct:[self.products objectAtIndex:index]];
vc.catalogViewController = self;
vc.productIndex = index;
return vc;
}
and you implement the goToProductAtIndex: method like this:
- (void)goToProductAtIndex:(NSInteger)index {
ProductViewController *vc = [self productViewControllerForProductAtIndex:index];
if (!vc)
return;
NSMutableArray *vcs = [[self.navigationController viewControllers] mutableCopy];
while (vcs.lastObject != self)
[vcs removeLastObject];
[vcs addObject:vc];
[self.navigationController setViewControllers:vcs animated:YES];
}
You can use the same method to handle a table row selection:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self goToProductAtIndex:indexPath.row];
}
If you want to get more software-engineery, you can create a protocol around the goToProductAtIndex: method and use that to avoid making ProductViewController know about the CatalogViewController class.

Related

one timer per detail view controller

I have a view controller with a table view property and a detail view controller connected to each cell in the tableview via the navigation bar. In the detail view controller is a countdown timer, with an interval specified when the user creates the task. I am trying to make it so each cell (or task) has a unique detail view controller. I am using core data. This is what I have now:
-(void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
if (!self.detailViewController) {
self.detailViewController =
[[DetailViewController alloc] initWithNibName:#"DetailViewController"
bundle:nil];
}
Tasks *task = [[self fetchedResultsController] objectAtIndexPath:indexPath];
self.detailViewController.testTask = task;
[[self navigationController] pushViewController:self.detailViewController
animated:YES];
}
DetailViewController.m
#interface DetailViewController : UIViewController <UIAlertViewDelegate>
#property (weak, nonatomic) IBOutlet UILabel *timeLeft;
#property (weak, nonatomic) IBOutlet UILabel *timerLabel;
#property (nonatomic, strong) Tasks *testTask;
#end
I feel like this is the correct way to implement the detail view controller because it minimizes the amount of memory that needs to be created, however it doesn't really suit my needs. Currently when a user taps a cell, and taps back, and taps a different cell, only the first cell's properties are loaded. Also, if I were to delete a cell there would be no way to invalidate its timer (i think) with this method. Any suggestions?
---edit---
I guess the question I should be asking is: How should I make it so that each Detail View has a decrementing label (that gets its information from a timer)?
Your best solution is to follow the MVC properly in this scenario. In your case you are storing data for each detailViewController you are creating (such as task and the countdown timer/interval etc).. and in rdelmar's answer he is suggesting that you store all the view controllers in a mutableArray. I disagree with both approaches as yours will have memory problems when you dismiss the view controller (as u've seen for yourself) and in rdelmar's case, you are storing viewControllers (along with the data they reference) in a mutable array.. which is wasteful.
think about it this way.. you want to keep track of the data in one place (that's unaffected with which view is on display right now.. it could be detailVC 1 or 100 or the VC with the tableView or whichever) and at the same time you want to allocate one detailVC at a time that simply displays whatever the data source tells it to display. That way you can scale your app (imagine what would happen if you had hundreds of indexes in your table.. will you store hundreds of view controllers? very expensive and redundant).
so simply create a singelton.. the singelton will have a mutableArray that stores the timers pertaining to each tapped table index and so on.. the singelton will also launch the timers every time a cell has been tapped and keep a reference to it (ie store the NSIndexPath), so that when you jump back and forth between detailVCs and the table.. the timers are still in operation as required by you (b/c they are referenced by the singelton). the DetailVc will simply ask the singelton for what it should display and display it.
hope this helps. Please let me know if you need any further clarification.
The trouble with your code is that you're only creating one instance of DetailViewController, so each cell is pushing to the same one. You have to have some way in didSelectRowAtIndexPath to look at the index path and use that to determine which instance of DetailViewController to go to. One way to do that would be to create a mutable dictionary to hold references to the instances of DetailViewController. You could have the keys be NSNumbers that correspond to the indexPath.row, and the value would be an instance of DetailViewController. So, your code might look something like this:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
DetailViewController *detailVC;
if (![self.detailControllersDict.allKeys containsObject:#(indexPath.row)]) {
detailVC = [[DetailViewController alloc]initWithNibName:#"DetailViewController" bundle:nil];
[self.detailControllersDict setObject:detailVC forKey:#(indexPath.row)];
}else{
detailVC = self.detailControllersDict[#(indexPath.row)];
}
Tasks *task = [[self fetchedResultsController] objectAtIndexPath:indexPath];
detailVC.testTask = task;
[[self navigationController] pushViewController:detailVC animated:YES];
}
detailControllersDict is property pointing to an NSMutableDictionary.

Xcode/Obj-c - Segue pushes new instance

I have seen similar questions a lot on Stackoverflow and I tried a lot of things but I can't seem to figure this out. I have multiple TableViewControllers and 1 MainViewController. The MainViewController has buttons calling the different TableViewControllers and on selecting a tablecell the tableViewController dismisses.
The problem is that im pushing a new instance of my MainViewController every time I push from either one of my tableViewControllers. I currently use Segues to push between these different controllers.
In short: When switching from TableViewControllers to ViewController I want to prevent the ViewController to get pushed as a new instance because this way its removing my previous data input.
Im pretty sure I have to use either:
[self dismissModalViewController: withCompletion:]
performSegue
prepareForSegue
Or set some global variables in a class and call those, but im not experienced enough yet to implement this correctly.
A simple example of end result would be: 3 textfields in VC. On clicking textfield1 it opens tableview1 and on clicking a cell it updates textfield1. Textfield2 opens tableview2, etc.
Hope im clear enough, could post sample code if needed.
Edit, posting code (keep in mind, segues are performed in storyboard):
TableViewExample.h:
#interface IndustryViewController : UIViewController <UITableViewDelegate, UITableViewDataSource> {
NSArray *tableViewArray;}
#property (nonatomic, strong) IBOutlet UITableView *tableViewIndustry;
TableViewExample.m:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"showIndustry"]) {
NSIndexPath *indexPath = [self.tableViewIndustry indexPathForSelectedRow];
ViewController *destViewController = segue.destinationViewController;
destViewController.industryText = [tableViewArray objectAtIndex:indexPath.row];
destViewController.industryTextName = [tableViewArray objectAtIndex:indexPath.row];
}}
Then in ViewController.m, viewDidLoad:
[industry setTitle:industryText forState:UIControlStateNormal];
These are the most important parts I think.
Is the segue of type "Push"? If so you should try dismissing the table view controllers using:
[self.navigationController popViewControllerAnimated:YES];
If the segue is of type "Modal" instead you should do something like this on your table view controller:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// your logic here
[self dismissModalViewControllerAnimated:YES];
}
As for the data exchange between controllers what I would personally do is creating a public property in the header file of the Table View Controller, like the following:
#property (nonatomic, weak) <Your_UIViewController_Subclass_Here> *mainController
Than, in the main controller, override the prepareForSegue:sender: method to set the newly created property to point to the main controller, like this:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
<Your_Subclass_Of_UITableViewController_Here> *destinationController = segue.destinationController;
destinationController.mainController = self;
}
Now the Table View Controller will have a pointer to the main controller to send the data basically all you have to do is to implement some public method or property in the Main Controller to be called when the user selects a table view row in the table view controller in order to update the text in the textfields or whatever data model you are using.

Add Objects From One View Controller To An Array On Another View Controller

I have been trying to figure this out for a while and not coming up with a solution. I have a view controller with a table and the first cell of the table is allocated for a button called "Add Friends". When clicked, it takes you to another view controller with a list of contacts in a table. When you click on a person, it goes back to the other view controller and adds the selected person. This is what I have so far.
ContactsViewController.m
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FirstViewController *newVC = [self.storyboard instantiateViewControllerWithIdentifier:#"newVCSegue"];
newVC.peopleArray = [[NSMutableArray alloc] init];
Person *user = [contactsList objectAtIndex:indexPath.row];
NSArray *userKeys = [NSArray arrayWithObjects:#"FirstName", #"LastName", nil];
NSArray *userObjects = [NSArray arrayWithObjects:user.firstName, user.lastName, nil];
NSDictionary *userDictionary = [NSDictionary dictionaryWithObjects:userObjects forKeys:userKeys];
[newVC.peopleArray addObject:userDictionary];
[self.navigationController pushViewController:newVC animated:YES];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
FirstViewController.h
#property (strong, nonatomic) NSMutableArray *peopleArray;
FirstViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//...
if (indexPath.row == 0) {
contactName.text = #"Add Person";
imgView.image = [UIImage imageNamed:#"plus-icon.png"];
} else {
NSString *firstName = [[peopleArray objectAtIndex:(indexPath.row)-1] objectForKey:#"firstName"];
NSString *lastName = [[peopleArray objectAtIndex:(indexPath.row)-1] objectForKey:#"lastName"];
contactName.text = [NSString stringWithFormat:#"%# %#", firstName, lastName];
}
return cell;
}
This lets me add one friend so far and if I decided to add another to the list, it replaces the first friend added.
What's basically happening is every time you select a new contact, you're recreating the array in the first view controller, hence it is replacing things. You ideally want to try and avoid getting the FirstViewController using the storyboard like that as well, it's pretty bad practice and may well lead to various problems later.
What I'd suggest in this situation is creating a protocol (look at the delegate pattern). This way, what you'd have is :
Use taps "Add Contact"
Contacts list appears, and FirstViewController is set as the delegate
User taps contact to add them
ContactsViewController informs the delegate of the user that was selected
FirstViewController adds the user, and dismissed the view controller
This is generally the approach you'd take, and it's pretty simple to implement. Start with the protocol
#protocol ContactsDelegate
-(void) contactsViewController:(ContactsViewController *)vc didSelectContact:(Person *)person;
#end
Then, make your FirstViewController implement this protocol. To do this, in your header file, in the angle brackets after the name (< >) add ContactsDelegate
In the implementation of FirstViewController, add the new method of the contacts delegate.
In your ContactsViewController.h file, add
#property (nonatomic, assign) NSObject<ContactsDelegate> *delegate;
Then when you display your contacts view controller, set the delegate
userVc.delegate = self;
[self presentModalViewController:userVc];
Then, in the user view controllers didSelectRowAtIndexPath:, simply inform the delegate that you've selected that person
[delegate contactsViewController:self didSelectContact:[contactsList objectAtIndex:indexPath.row]];
And lastly, in your FirstViewController, in the delegate method you added, we need to ADD the user to the list, not re-create the list
[peopleArray addObject:person];
And that should do what you're after :)
From what I understand, you are instantiating a new FirstViewController every time you select a contact in the ContactsViewController. Instead, you should reference the original FirstViewController (perhaps save it before transitioning to ContactsViewController), and use this reference to add the contact to the original array [original.people addObject:userDict]. As long as you make sure to reload the table, this should work.

UISwitch in a first view and a label in a second

So, I want to place a label in my fist view and place in a second one a UISwitch.
But the problem is I can't link together everything.. :/
in my first view i have that
- (void)onRoff {
if (mySwitch1.on) {
test.hidden = YES;
}
else (test.hidden = NO);
}
but here I have an error with mySwitch1 because it's declared in my secondView..
I don't know if it's clear, I want to link a label and a switch in different view..
Thanks !
Indeed you are not very clear. The first thing you might want to try is describe what you did:
how are your two views instantiated?
Let's assume your two views are instantiated from two different nib files.
what is the object you want to have access to your label and switch?
Let's assume it's a view controller. It's a bit unusual for a single view controller to control two views from two different nib files, but after all, why not?
In any case, you can set the owner class for your two nib files to be the class of your view controller. Then in Interface Builder, from the first view, you can bind the label to the file owner's UILabel outlet. And in Interface Builder, from the second view, you can bind the UISwitch to the file owner's second outlet, of type UISwitch.
But perhaps the onRoff methods of yours is actually a method of one of your two view class? The same idea apply: you can set the file owner in the second nib file to be the view class of the first view, and then bind the switch to the file owner's UISwitch outlet.
But it sounds like your design might be worth working on...
Edit: after your comment, here is a bit more...
The problem is that your two view controllers each control a different page and have no reason to know about each other. So you need a middle man object. That could be another controller. Let's use the Application delegate. Then, in the IBAction method of your SwitchViewController, you can do something like:
- (IBAction) switchChangedValue:(UISwitch *) sender {
NSString *newLabelText = sender.isOn ? #"On" : #"Off";
self.labelViewController.label.text = newLabelText;
}
Now how will everybody know about each other? First each view controller will inform the middle man. Here is it for the SwitchViewController:
- (void) viewDidLoad
{
[super viewDidLoad];
MyAppDelegate *appDelegate = (MyAppDelegate *)[[UIApplication sharedApplication] delegate];
appDelegate.switchViewController = self;
}
Second, the app delegate will need to coordinate everything:
#interface MyAppDelegate : …
#property (nonatomic, retain) SwitchViewController *switchViewController;
#property (nonatomic, retain) LabelViewController *labelViewController;
#end
#implementation MyAppDelegate
#synthesize switchViewController = _switchViewController;
#synthesize labelViewController = _labelViewController;
- (void) setSwitchViewController:(SwitchViewController *) newSwitchController {
if (newSwitchController != _switchViewController) {
[_switchViewController release];
_switchViewController = [newSwitchController retain];
_switchViewController.labelViewController = _labelViewController;
if (_labelViewController)
_labelViewController.label.text = _switchViewController.switch.isOn ? #"On" : #"Off";
}
}
- (void) setLabelViewController:(LabelViewController *) newLabelController {
if (newLabelController != _labelViewController) {
[_labelViewController release];
_labelViewController = [newLabelController retain];
_labelViewController.switchViewController = _switchViewController;
if (_switchViewController)
_labelViewController.label.text = _switchViewController.switch.isOn ? #"On" : #"Off";
}
}
I left out a number of details, but I hope the big picture is clear.
So you have declared ur UISwitch in the second view and ur label in the first view. All u have to do is just use NSUserDefaults to achieve wat u want. Have the following method in the second view itself. Dont bring it to the first view.
- (void)onRoff {
if (mySwitch1.on) {
[[NSUserDefaults standarduserdefaults]setObject:#"off" forKey:#"state"];
[[NSUserDefaults standarduserdefaults]synchronize];
}
else {
[[NSUserDefaults standarduserdefaults]setObject:#"on" forKey:#"state"];
[[NSUserDefaults standarduserdefaults]synchronize];
}
}
Now in the viewWillAppear method of the first view just chk the value of the NSUserDefaults..
-(void)chkState{
NSString *tempStr=[[NSUserDefaults standarduserdefaults]objectForKey:#"state"];
if([tempStr isEqualTo:#"on"]) {
test.hidden=YES;
}
else {
test.hidden=NO;
}
}
Call this method in the viewWillAppear of the firstview like this....
[self chkState];
Hope this helps....If u want save the state of the switch too then just chk the userdefaults value again in the viewWilAppear method of the 2nd view and based

iphone - access uitableviewcontroller from uitableviewcell

I have a structure like this....
UITableViewController -> UITableViewCell -> UIView
I need the UIView to access a HashTable (NSMutableDictionary) in the UITableViewController
Is there a way to access the ViewController simply from the ViewCell (using [ViewCell superview] obviously won't work) ....Do I need to go down through the AppDelegate using [[UIApplication sharedApplication] delegate]?
Thanks!
This should do the trick:
UITableView *tv = (UITableView *) self.superview.superview;
UITableViewController *vc = (UITableViewController *) tv.dataSource;
I usually maintain a weak reference from my UIView to my UIViewController if I need one, usually by creating a method something like this:
-(MyView*)initWithController:(CardCreatorViewController*) aController andFrame:(CGRect)aFrame
{
if (self = [super initWithFrame:aFrame])
{
controller = aController;
// more initialisation here
}
return self;
}
You could also use a delegate pattern if you want a more decoupled solution. I tend to think this is overkill for a view and its controller, but I would use it with a system of controllers and subcontrollers.
You can create a category for this:
#implementation UITableViewCell (FindTableViewController)
- (id<UITableViewDataSource>)tableViewController
{
UIView *view = self;
while (!(view == nil || [view isKindOfClass:[UITableView class]])) {
view = view.superview;
}
return ((UITableView *)view).dataSource;
}
#end
Then you can simply access self.tableViewController from the cell itself (assuming you have included this category). You may need to cast it to your table view controller's class tho.
Since the UITableViewCell is somewhere in the view hierarchy, you can access your root view by retrieving view.superview until you get it. If you don't want to add any properties to your view, you can access its controller through the view's nextResponder property. You would have to cast it to whatever class you need, of course, and it may not be the cleanest use of the property. It's a quick-n-dirty hack to get to it.
If you're looking for something you can show your children though, I'd aim for going through your app delegate, or if your view controller happens to be a singleton, just implement the singleton design pattern and access it through that.
Some modification from Kare Morstol answer :
The hierarchy of tableviewcell is in iOS 5.0(my test version)
cell.superview = tableview
cell.superview.superview = UIViewControllerWrapperView
So, use cell.superview to get tableview. And the tableview and tableviewController has a relation of delegate, dataSource in default.
You can get tableviewController reference by tableview.delegate or tableview.dataSource.
UITableView *tableView = cell.superview;
UITableViewController *tableViewController = tableView.delegate; // or tableView.dataSource`