How to get a reference to the view controller of a superview? - objective-c

Is there a way to get a reference to the view controller of my superview?
There were several instances that I needed this on the past couple of months, but didn't know how to do it. I mean, if I have a custom button on a custom cell, and I wish to get a reference of the table view controller that controls the cell I`m currently in, is there a code snippet for that? Or is it something that I should just solve it by using better design patterns?
Thanks!

Your button should preferably not know about its superviews view controller.
However, if your button really needs to message objects that it shouldn't know the details about, you can use delegation to send the messages you want to the buttons delegate.
Create a MyButtonDelegate protocol and define the methods that everyone that conforms to that protocol need to implement (the callback). You can have optional methods as well.
Then add a property on the button #property (weak) id<MyButtonDelegate> so that any class of any kind can be set as the delegate as long as it conforms to your protocol.
Now the view controller can implement the MyButtonDelegate protocol and set itself as the delegate. The parts of the code that require knowledge about the view controller should be implemented in the delegate method (or methods).
The view can now send the protocol messages to its delegate (without knowing who or what it is) and the delegate can to the appropriate thing for that button. This way the same button could be reused because it doesn't depend on where it is used.

When I asked this question I was thinking of, in a situation where I have custom cells with buttons on them, how can the TableViewController know which cell's button was tapped.
More recently, reading the book "iOS Recipes", I got the solution:
-(IBAction)cellButtonTapped:(id)sender
{
NSLog(#"%s", __FUNCTION__);
UIButton *button = sender;
//Convert the tapped point to the tableView coordinate system
CGPoint correctedPoint = [button convertPoint:button.bounds.origin toView:self.tableView];
//Get the cell at that point
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:correctedPoint];
NSLog(#"Button tapped in row %d", indexPath.row);
}
Another solution, a bit more fragile (though simpler) would be:
- (IBAction)cellButtonTapped:(id)sender
{
// Go get the enclosing cell manually
UITableViewCell *parentCell = [[sender superview] superview];
NSIndexPath *pathForButton = [self.tableView indexPathForCell:parentCell];
}
And the most reusable one would be to add this method to a category of UITableView
- (NSIndexPath *)prp_indexPathForRowContainingView:(UIView *)view
{
CGPoint correctedPoint = [view convertPoint:view.bounds.origin toView:self];
return [self indexPathForRowAtPoint:correctedPoint];
}
And then, on your UITableViewController class, just use this:
- (IBAction)cellButtonTapped:(id)sender
{
NSIndexPath *pathForButton = [self.tableView indexPathForRowContainingView:sender];
}

If you know which class is the superview of your view controller, you can just iterate through the subviews array and typecheck for your superclass.
eg.
UIView *view;
for(tempView in self.subviews) {
if([tempView isKindOfClass:[SuperViewController class] ])
{
// you got the reference, do waht you want
}
}

Related

What is the best way/design to handle touches as well UICollectionViewDelegate methods

I have functioning UICollectionView controller with my own UICollectionView subclass and cells etc.
1)Now I decided to add slideView as explained in http://www.raywenderlich.com/32054/how-to-create-a-slide-out-navigation-like-facebook-and-path behind by own collectionview controller.
2) Then I wasnt happy enough since it doesnt give me "peak-inside" feel as FaceBook app on iOS
3) So then I modified code and handle touches in my own collectionView subclass. After going thru this site and some initial struggle I can now get the same feel like FB
4) But in the process of handling touches I lost the UICollectionViewDelegate didSelectItemAtIndePath. They never get called because of touch.
5) So I added tapGesture on collectionView in my storyboard and hooked everything up and made explicit call
- (IBAction)selectCellWithSingleTap:(UITapGestureRecognizer *)sender
{
NSLog(#"SELECT");
CGPoint tapLocation = [sender locationInView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:tapLocation];
if (indexPath) {
[self collectionView:self.collectionView didSelectItemAtIndexPath:indexPath];
}
}
1)Now IS this the correct approach?
2)If not what is the better design?
3)I also like to know what this method does selectItemAtIndexPath:animated:scrollPosition: in UICollectionView could I use this one?

Segue from callout in MKMapView

Hi I have a mapView that has annotations pop up, I want to be able to segue when the annotation callout button is clicked. I have some problems though when I do it. I have a few questions
1) Do I have to embed the mapViewController in a navigation Controller? If yes, my annotations do not show up when I do, how come?
2) does prepareforsegue get called from performSegueWithIdentifier?
3) when u send self, in this case what would self be?
Thanks
- (void)mapView:(MKMapView *)mapView annotationView:(MKAnnotationView *)view calloutAccessoryControlTapped:(UIControl *)control
{
[self performSegueWithIdentifier:#"Present Photo" sender:self];
}
Realized the problem it occurs here, I used to get a map controller from the id detail but now I think its a navigation controller, how do I get reference to the map controller now?
-(void) updateSplitViewDetail{
// ERROR OCCURS HERE!!! No longer map controller since I embed in navigation controller
id detail = [self.splitViewController.viewControllers lastObject];
if ([detail isKindOfClass:[MapViewController class]]) {
MapViewController *mapVC = (MapViewController*) detail;
mapVC.delegate = self;
mapVC.annotations = [self mapAnnotations];
}
}
- (void)viewDidLoad
{
[super viewDidLoad];
[self updateSplitViewDetail]; //Error may be here
}
1)
Yes. If you want to perform a push segue, the source view controller (your map view controller) should be embedded in in navigation controller.
I'm not sure why your annotations/callouts aren't appearing in that case -- I've seen plenty of projects that work correctly that way. Perhaps your reference to the map view when you add the annotations isn't what you think it is? (And you're adding annotations to nil instead?) You'll need to provide more details for us to help. (Edit your question or post a new question since it's sort of a separate issue.)
2)
Yes. prepareForSegue:sender: is called after you call performSegueWithIdentifier:sender:.
3)
The "sender" argument in these methods is entirely for your own use -- its sole reason for existence is to allow you to pass some context from the code that calls performSegueWithIdentifier:sender: to the implementation of prepareForSegue:sender:. (Or in the case of segues automatically performed when the user taps some control, to allow your prepareForSegue:sender: implementation to know which control was tapped.)
So, pass whatever you want: self is fine, and so is nil if you're not making use of it. Or if it's useful for your prepareForSegue:sender implementation to know which callout was tapped, you might consider passing the annotation view's annotation as "sender" (say, so it can set up the destination view controller with appropriate info).

Objective-C (iOS): prepareForSegue won't pass my data into destination VC

VC1 = NewGameViewController
VC2 = GameViewController
NewGameViewController.m
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if( [segue.identifier isEqualToString:#"newGameSegue"]) {
GameViewController *gameVC = (GameViewController *)segue.destinationViewController;
NSArray *array = [self nameArrayForTextFieldArray:self.namePicker.textFieldArray withColon:YES];
gameVC.nameArray = [[NSArray alloc] initWithArray:array];
}
-(NSArray *)nameArrayForTextFieldArray:(NSArray *)array withColon:(BOOL *)bool
basically returns an nsarray of strings given an nsarray of textfields. withcolon is a bool of whether or not you want the strings to have a colon appended to the end.
when i debug my code, the _nameArray ivar in gameVC still reads nil after every line here is called...can anyone help me out here??
The prepareForSegue method is invoked by UIKit when a segue from one screen to another is about to be performed. It allows us to give data to the new view controller before it will be displayed. Usually you’ll do that by setting its properties.
The new view controller can be found in segue.destinationViewController. If GameViewController embed the navigation controller, the new view controller will not be GameViewController but the navigation controller that embeds it.
To get the GameViewController object, we can look at the navigation controller’s topViewController property. This property refers to the screen that is currently active inside the navigation controller.
To send an object to the new view controller you can use this solution using performSegueWithIdentifier:
For example, if we want to perform a segue pressing a UIButton we can do this:
In the MyViewController.h we create a IBAction (connected to UIButton), dragging the button from storyboard to code:
- (IBAction)sendData:(id)sender;
In MyViewController.m we implement the method:
- (IBAction)sendData:(id)sender
{
NSArray *array = [self nameArrayForTextFieldArray:self.namePicker.textFieldArray withColon:YES];
[self performSegueWithIdentifier:#"newGameSegue" sender:array];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"newGameSegue"]) {
UINavigationController *navigationController = segue.destinationViewController;
GameViewController *controller = (GameViewController *)navigationController.topViewController;
controller.nameArray = sender;
}
}
Is GameViewController embedded in a navigation controller? In that case, your destinationViewController property is of type UINavigationController, not GameViewController. You can get to GameViewController by calling [segue.destinationViewController.viewControllers lastObject].
I'm assuming that you've done a NSLog (or examine it in the debugger) of array immediately before setting gameVC.nameArray. You really want to make sure it's being set the way you think it is. It's amazing how many times I've spent debugging something like this only to realize the problem was in my equivalent to nameArrayForTextFieldArray. Or a typo in the name of the segue identifier. Or random things like that.
Assuming that's ok, then a couple of things are possible:
How is your nameArray property defined in GameViewController? If it's not a strong reference (or a copy), then when your array falls out of scope, it will be deallocated. I think this would manifest itself slightly differently, but it's worth confirming.
Also, I've seen situations where a controller like GameViewController might have some confusion between various ivars and properties (which is why I never define ivars for my properties ... I let #synthesize do that).
I assume you're not using a custom setter for nameArray. I just want to make sure. If so, though, please share that, too.
Bottom line, can you show us all references to nameArray in your #interface of GameViewController as well as in its #synthesize statement?

How to present a view controller from another view controller

I am trying to open a ViewController from within another ViewController if certain conditions are met. The code seems to run without error but the view is never shown. I am new to xcode 4 /ios 5 so I must be missing something.
Here is the code responsible for opening the second viewcontroller:
CreateUserViewController *createUserController = [[CreateUserViewController alloc] initWithNibName:#"CreateUserView" bundle:[NSBundle mainBundle] keyWrapper:keyChainWrapper];
[self presentViewController:createUserController animated:YES completion:nil];
In my project I have a xib called, "CreateUserView". I have added a view controller to this xib and assigned it to, "CreateUserViewController".
Also I noticed in the apple documentation that is shows setting the delegate of the viewcontroller to be presented. But it seems that no property called, "delegate" is on the viewcontroller object. Is this documentation old? This is the document I am trying to use (section 9-1):
View Controller Programming
Can someone give me a hint? Thanks..
edit Adding Custom Constructor
-(id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil keyWrapper:(KeychainItemWrapper *)keyWrapper
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if(self){
[self setKeyChainWrapper:keyWrapper];
}
return self;
}
Regarding CreateUserView.xib: you don't want to put a CreateUserViewController object in the nib. You want to set the custom class of the File's Owner placeholder to CreateUserViewController. Then you need to connect the view outlet of File's Owner to the top-level view in the nib.
Regarding the delegate property: The UIViewController class doesn't have its own delegate property. The idea is that you add a delegate property to your subclass of UIViewController. The delegate provides a way for your presented view controller to pass custom information back to the presenting view controller.
Why would you want to do that? Let's consider the code you posted. I'll assume you have a UserListViewController that shows a list of User objects, and has a "Create new user" button. When the user touches the "Create new user" button, you create a CreateUserViewController and present it.
The user interacts with the CreateUserViewController to set the attributes of the new User object - name, rank, hairstyle, etc. Then he touches a "Done" button. Your CreateUserViewController creates the new User object and puts it in the database. Then it needs to dismiss itself, so the UserListViewController's list of User objects will appear again.
But you want the User list to include the newly created User object and you want to scroll the list so that the new User is on the screen. So you need a way to have your CreateUserViewController tell the UserListViewController about the newly created User object. This is where the delegate comes in.
You define a protocol like this:
#protocol CreateUserViewControllerDelegate
- (void)didCreateUser:(User *)user;
#end
and you give your CreateUserViewController a delegate property:
#interface CreateUserViewController
#property (weak, nonatomic) id<CreateUserViewControllerDelegate> delegate;
// ...
When your CreateUserViewController's "Done" button is touched, you notify your delegate of the new User:
- (IBAction)doneButtonWasTouched:(id)sender {
User *user = [self createUser];
[self.delegate didCreateUser:user];
[self dismissViewControllerAnimated:YES completion:nil];
}
In your UserListViewController, you adopt and implement the protocol:
#interface UserListViewController <CreateUserViewControllerDelegate, UITableViewDelegate, UITableViewDataSource>
// ...
#end
#implementation UserListViewController
- (void)didCreateUser:(User *)user {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[self.users count] inSection:0];
[self.users addObject:user];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation: UITableViewRowAnimationAutomatic];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition: UITableViewScrollPositionNone animated:YES];
}
and when you need to present a CreateUserViewController, you set the new controller's delegate to the UserListViewController:
- (IBAction)createUserButtonWasTouched:(id)sender {
CreateUserViewController *createUserController = [[CreateUserViewController alloc] initWithNibName:#"CreateUserView" bundle:[NSBundle mainBundle] keyWrapper:keyChainWrapper];
createUserController.delegate = self;
[self presentViewController:createUserController animated:YES completion:nil];
}
In iOS5 the method for pushing new view controllers was really changed around quite a bit from iOS4 and Xcode 3. In summary, storyboards are now used to create your application view controller flow. Even though you may use standalone .xib files to build an application it is much less common in iOS5.
Anyway, the main method for pushing new view controllers onto the screen is done using segues. Check out this tutorial for an introduction: http://www.raywenderlich.com/5138/beginning-storyboards-in-ios-5-part-1
It does a good job on explaining how to create a storyboard and use segues. You can still present view controllers in code "the old way" but it is much much less common now with the introduction of these new technologies. There are also some absolutely awesome tutorials on iTunes U - search for CS193P. It's the Stanford Introductory class to Objective-C and programming for iOS. This should get you started and maybe help you think of a way to push your createUserController in a way more up to speed with iOS5.
UPDATE
I just wanted to add. If you configure your program to use storyboards and segues you can use the method performSegueWithIdentifier:sender: to perform the segue to your createUserController view if the proper conditions are met. See the Apple API for UIViewController for information on how to use this method.

NSTableView keyDown: and mouseDown:

I've been working on a menubar note-taking application for Mac. It is written in Objective-C and Cocoa, and I'm using BWToolkit with it. My problem is getting keyDown: and mouseDown: events in a BWTransparentTableView which is a subclass of NSTableView. I just can't get it to work. I've tried searching the Internet, and some places say that you must subclass NSTableView. I've tried that, but it still doesn't work. I am pretty new to Objective-C and Cocoa, and I may just be doing something incorrectly.
Items in an NSTableView will automatically begin editing when they are slow double-clicked or when the Return key is pressed. Make sure that the table view, the cell and the array controller (if used) are marked as editable.
If you are not using an NSArrayController, make sure that your table view has a delegate and that it responds to tableView:shouldEditTableColumn:row:.
To handle a double click, you just need to set the doubleAction of the table view:
- (void)awakeFromNib
{
[tableView setTarget:self];
[tableView setDoubleAction:#selector(doubleClickInTable:)];
}
- (void)doubleClickInTable:(id)sender
{
NSInteger rowIndex = [sender selectedRow]; //Use selectedRowIndexes if you're supporting multiple selection
//Handle the double click
}
Note that neither of these methods require you to subclass NSTableView.