Pass variable to view controller before viewDidLoad - objective-c

I perform a segue which is defined in the Storyboard to open a new view controller. I need to configure the destination view controller of the segue in a special state where some of it's buttons does not needed to be displayed.
I know that I can do this by setting a variable on this controller in my source view controller's -prepareForSegue:sender:. The problem with this is, that firstly it instantiates the controller, so it's -viewDidLoad: will run, then only after can I set anything on it.
I can't create the controller entirely from code, because it's user interface is in Storyboard. -instantiateViewControllerWithIdentifier: also calls -viewDidLoad first obviously.
I could probably use a semaphore and add the initialization code into my destination controller's -viewWillAppear, but that's ugly, it has to be some more elegant way to do this than doing a check every time the view appears. (My settings need to be done only once.)
Is there some way to pass variables into the controller before it's -viewDidLoad runs?
EDIT: It looks like this happens only if I trigger the segue from code using -performSegueWithIdentifier:.

On my machine and on iOS 8.0 and iOS 9.0, viewDidLoad is called after prepareForSegue. So, something like the following worked for my test case of your answer.
In your source controller:
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
TimViewController * controller = segue.destinationViewController;
if( [controller isKindOfClass:[TimViewController class]] )
controller.name = #"tim";
}
In your destination controller (TimViewController):
- (void)viewDidLoad
{
[super viewDidLoad];
// Do view setup here.
NSLog( #"view did load %#", self.name );
}
Add a segue (a show segue) from your source control to the destination view controller.
Output:
2015-09-17 19:09:04.351 Test[51471:7984717] view did load tim

I think there is some confusion here. -prepareForSegue:sender: gets called before -viewDidLoad gets called. Please double check your implementation.
Edit:
May be this thread will help you understand this and one of the mentioned cases fall in your case.

Related

Object becomes Nil when pushing controller

I have a the first application controller, MAViewControllerMenu, and when that controller loads, I already allocate the next controller, imageControllerView.
- (void)viewDidAppear{
[super viewDidAppear:(YES)];
if (!imageControllerView)
imageControllerView = [[self storyboard] instantiateViewControllerWithIdentifier:#"chosenImageController"];
}
Then, I select an image from the image picker, and want to move to the next controller,imageControllerView, where the image would be displayed. I set the next window's image property as follows:
imageControllerView.image = [[self.pageViews objectAtIndex:(centered_image_ind)] image];
This line works, I checked that there's a value in imageControllerView.image.
However, when I move to the next controller,imageControllerView , I notice that the memory address of imageControllerView changes, or in other words, imageControllerView's properties that I change before moving to that controller, specifically image, reset when I move there.
Instead of throwing code here, I was hoping you could let me know what I should provide.
I think it's a common problem people know of:
Controller's objects re-init'ing when moving from one controller to another.
Here's a screen shot that might give a hint of what Im trying to do
Left most one is where I select pictures which in turn go into the slide show scrollview. Then I click next, and the image is supposed to appear in the centered ImageView
Thanks
OK...
You cannot "already allocate the next view controller" this won't work. There is no point in creating it like this at all. You can delete the imageViewController property (or iVar) completely.
The arrows that you have between the view controllers in your storyboard are segues. In Interface Builder you can select a segue and give it an identifier. For instance you would use something like #"ImageViewSegue".
I guess the segue is attached to the Next button. This is fine.
Now, in your MAViewControllerMenu you need to put this method...
- (void)prepareForSegue:(UIStoryBoardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"ImageViewSegue"]) {
// the controller is CREATED by the segue.
// the one you create in view did load is never used
ImageViewController *controller = segue.destinationController;
controller.image = [[self.pageViews objectAtIndex:(centered_image_ind)] image];
}
}
Now for the segues in the other direction...
You appear to be using segues to dismiss the modal views. You can't do this. What it will do is create a new view controller and present that instead of dismissing the presented view.
i.e. you'll go...
A -> B -> C -> B -> A -> B
// you'll now have 6 view controllers in memory
// each segue will create a fresh view controller with no values set.
What you want is...
A -> B -> C
A -> B
A
// now you only have one view controller because the others were dismissed.
// when you dismiss a view controller it will go back to the original one.
// the original one will have all the values you set previously.
To do this you need to create a method something like...
- (IBAction)dismissView
{
[self dismissViewControllerAnimated:YES completion:nil];
}
Then whatever the button is for your dismiss action attach it to this method.
Now delete all the backwards curly segues.
Passing info back
To pass info back to the original view controller you need a delegation pattern or something similar.
You can read more about creating a delegate at This random Google Search
Create a delegate method something like...
- (void)imageViewSelectedImage:(UIImage *)image;
or something like this.
Now when you do prepareForSegue you can do...
controller.delegate = self;
and have a method...
- (void)imageViewSelectedImage:(UIImage *)image
{
// save the method that has been sent back into an array or something
}
I might be wrong, but seems you go to your second view controller using a segue, it is normal your controller instance isn't the same than the one retrieved by [[self storyboard] instantiateViewControllerWithIdentifier:#"chosenImageController"]
you should take a look at - (void) prepareForSegue:(UIStoryboardSegue *)segue
(UIViewController method)
inside this method set your image property to the segue destination controller (check the identifier of the segue)

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

NSTextField set cursor

Okay so I feel like there's something obvious I'm missing in this question. I've used makeFirstResponder throughout my code to move from textField 1 to 2, 2 to 3, etc. That seems to work as I want it to, yet when the new view is loaded, I want the cursor to be in textField1, and yet the following code does not place the cursor in textField1 upon load.
- (void) awakeFromNib{
[[[self view] window] makeFirstResponder:textField1];
}
I also tried setInitialFirstResponder, and that didn't have any effect either (I don't even think that would be right.) So, is it because it is in the awakeFromNib method? Can anyone tell me what I'm missing? Thanks in advance.
EDIT - My solution was differed slightly from the accepted answer so I thought I'd post my implementation. Because the view I wanted to set the first responder for was a subview added later (think the second screen of an application wizard), I simply added a setCursorToFirstTextField method:
- (void) setCursorToFirstTextField {
[[[self view] window] makeFirstResponder:textField1];
}
And made sure to call it after I had added the subview to the custom view on the original window.
Yes, you're right about the problem being the location of the method in awakeFromNib. If you log [self.view window] in your awakeFromNib, you'll see that it's NULL. I don't know how exactly you have things set up, but I'm guessing (if this relates to your WizardController question) that you're doing an alloc initWithNibName:bundle: in another class to create your view controller and then adding that controller's view to the view hierarchy. If you throw some logs in there, it will show you that awakeFromNib in the controller class is called after the alloc init, but before the view is added as a subview, so there is no window at that time. The way I got around this problem was to create a setup method in the view controller class (with the makeFirstResponder code in it), and call it from the class where you create the controller after you add it as a subview.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
self.wizard = [[WizardController alloc] initWithNibName:#"WizardController" bundle:nil];
[self.window.contentView addSubview:wizard.view];
[self.wizard doSetup];
}

Replacing Storyboard Segue with pushViewController causes strange behaviour

I can't seem to figure this out for the life of me. I have a custom table view cell, in that cell I have a few buttons configured. Each button connects to other view controllers via a storyboard segue. I've recently removed these segues and put a pushViewController method in place. Transition back and forth across the various views works as expected however the destination view controller is not displaying anything! I have some code below as an example.
Buttons have this method set:
[cell.spotButton1 addTarget:self action:#selector(showSpotDetails:) forControlEvents:UIControlEventTouchUpInside];
// etc...
[cell.spotButton4 addTarget:self action:#selector(showSpotDetails:) forControlEvents:UIControlEventTouchUpInside];
// etc...
showSpotDetails Method contains this code:
- (void)showSpotDetails:(id)sender
{
// determine which button (spot) was selected, then use its tag parameter to determine the spot.
UIButton *selectedButton = (UIButton *)sender;
Spot *spot = (Spot *)[spotsArray_ objectAtIndex:selectedButton.tag];
SpotDetails *spotDetails = [[SpotDetails alloc] init];
[spotDetails setSpotDetailsObject:spot];
[self.navigationController pushViewController:spotDetails animated:YES];
}
The details VC does receive the object data.
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"spotDetailsObject %#", spotDetailsObject_.name);
}
The NSLog method below does output the passed object. Also, everything in the details view controller is as it was. Nothing has changed on the details VC. It just does not render anything ever since I removed the segue and added the pushViewController method. Perhaps I am missing something on the pushViewController method? I never really do things this way, I try to always use segues...
Any suggestions?
Welcome to the real world. Previously, the storyboard was a crutch; you were hiding from yourself the true facts about how view controllers work. Now you are trying to throw away that crutch. Good! But now you must learn to walk. :) The key here is this line:
SpotDetails *spotDetails = [[SpotDetails alloc] init];
SpotDetails is a UIViewController subclass. You are not doing anything here that would cause this UIViewController to have a view. Thus you are ending up a with blank generic view! If you want a UIViewController to have a view, you need to give it a view somehow. For example, you could draw the view in a nib called SpotDetails.xib where the File's Owner is an SpotDetails instance. Or you could construct the view's contents in code in your override of viewDidLoad. The details are in the UIViewController documentation, or, even better, read my book which tells you all about how a view controller gets its view:
http://www.apeth.com/iOSBook/ch19.html
The reason this problem didn't arise before is that you drew the view in the same nib as the view controller (i.e. the storyboard file). But when you alloc-init a SpotDetails, that is not the same instance as the one in the storyboard file, so you don't get that view. Thus, one solution could be to load the storyboard and fetch that SpotDetails instance, the one in the storyboard (by calling instantiateViewControllerWithIdentifier:). I explain how to do that here:
http://www.apeth.com/iOSBook/ch19.html#SECsivc

prepareForSegue is not called after performSegue:withIdentifier: with popover style

I have a universal app, where I am sharing the same controller for a IPad and IPhone storyboard.
I have put a UILongPressGestureRecognizer on a UITableView, that when a cell is pressed on iPhone it calls an action that perform a segue:
-(IBAction)showDetail:(id)sender {
UILongPressGestureRecognizer *gesture = (UILongPressGestureRecognizer*)sender;
if (gesture.state == UIGestureRecognizerStateBegan) {
CGPoint p = [gesture locationInView:self.theTableView];
NSIndexPath *indexPath = [self.theTableView indexPathForRowAtPoint:p];
if (indexPath != nil) {
[self performSegueWithIdentifier:SEGUE_DETAIL sender:indexPath];
}
}
}
the segue is a detail view performed as a 'push'. The first thing you should notice is that the sender is an NSIndexPath, is the only way I found for passing the selected cell. Maybe there's a better solution.
Everything works fine, in a sense that the segue is performed, and before the prepareForSegue is called too.
However it happens that on iPad, I have changed the segue identifier to Popover.
Now things are working in part, the segue is performed, but prepareForSegue is not called and therefore the destination view controller is not set up as it should be.
What am I doing wrong ?
What I have discovered so far, is that with any segue identifier that is not popover these are the invocations made by iOS:
prepareForSegue (on source controller)
viewDidLoad (on destination controller)
while in popover segue the invocation order is:
viewDidLoad (on destination controller)
prepareForSegue (on source controller)
just because I put all my logic in viewDidLoad, the controller was not properly initialized, and a crash happened. So this is not exactly true that prepareForSegue is not called, the truth is that I was getting an exception, and I wrongly mistaken as prepareForSegue not getting called.
I couldn't put everything in viewWillAppear because a call to CoreData had to be made and I didn't want to check if entities were ok each time the view display.
How did I solve this ? I created another method in destination controller
-(void)prepareViewController {
// initialization logic...
}
and changing the prepareForSegue method in source controller itself:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
MyViewController *mvc = (MyViewController*)[segue destinationViewController];
// passing variable
// with segue style other than popover this called first than viewDidLoad
mvc.myProp1=#"prop1";
mvc.myProp2=#"prop2";
// viewWillAppear is not yet called
// so by sending message to controller
// the view is initialized
[mvc prepareViewController];
}
don't know if this is expected behavior with popover, anyway now things are working.
I've noticed that the boiler plate code for Xcode's Master-Detail template (iPhone) uses the following pattern for configuring the detail VC's view:
the detail VC's setters (for properties) are overwritten in order to invoke the configureView method (configureView would update all your controls in the view, e.g., labels, etc.)
the detail VC's viewDidLoad method also invokes the configureView method
I did not follow this pattern the other day when I was trying to re-use a detail VC in my movie app, and this gave me trouble.
I don't have much experience with popovers; however, if the pattern above is used with a detail VC that is displayed inside a popover, then wouldn't the detail VC's view get configured when you set the detail VC's properties from within the prepareForSegue method?