IBOutlet properties does not update when using prepareForSegue method

I am having problem passing value to a IBOutlet property of destinationViewController but it works fine on ordinary property see code below

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:@"NewsCellToDetail"]) {        
    testViewController *viewController = segue.destinationViewController;
    viewController.titleLabel.text = @"test"; // set the IBOutlet label text to something
    NSLog(@"%@",viewController.titleLabel.text); // this will output to nil
    viewController.textTest = @"testing2"; // set the property to something
    NSLog(@"%@", viewController.textTest) // this will output the string testing2
}

This is the code for the header file testviewcontroller.h

#import <UIKit/UIKit.h>
@interface NewsDetailViewController : UIViewController
@property (strong, nonatomic) IBOutlet UILabel *titleLabel;
@property (strong, nonatomic) NSString *textTest;
@end

I already synthesize both the property.

Thanks for the help.


I'm a little late coming into this answer, but I ran into this issue recently, and the reason for this would have been obvious if we were still instantiating things manually instead of letting the storyboard handle it. It so happens to be the same reason you never manipulate the view when manually instantiating view controllers: the segue's destinationViewController has not called loadView: yet, which in a storyboard runs through deserializing all the view objects from the associated nib.

An extremely simple way to see this in action:

  1. Create a two ViewController Scenes (ViewController1 and ViewController2)
  2. Add a button to ViewController1 and an action segue from the button to ViewController2
  3. Add a subview to ViewControler2, and an IBOutlet to that subview
  4. In prepareForSegue: of ViewController1, try to reference that subview outlet of ViewController2 - you'll find it's nil and its frame/bounds are null.

This is because ViewController2's view has not yet been added to view stack, but the controller has been initialized. Thus, you should never try to manipulate ViewController2's view in prepareForSegue: or otherwise anything you do will be lost. Reference Apple's ViewController Programming Guide here for the lifecycle: https://developer.apple.com/library/ios/featuredarticles/ViewControllerPGforiPhoneOS/ViewLoadingandUnloading/ViewLoadingandUnloading.html

The accepted answer here is forcing loadView to run in prepareForSegue: by accessing the .view property of the destination which is why things show up out of order, and will have unknown / difficult to reproduce results if you're trying to do any kind of view manipulation in viewDidLoad to account for any data loads, because without having the view loaded to the viewStack, any references to the parent view for frame references will be nil.

TL;DR; - If you have data that needs to be passed like in the OP's case, set it using public properties on the destination, and then in viewDidLoad of the destination controller, load that data into the view's subviews.

Edit:

Similar question here - IBOutlet is nil inside custom UIView (Using STORYBOARD)

You may also want to utilize viewDidLayoutSubviews: for any subview manipulation.


I just got the same problem recently. But when I debug it step by step, I find a possible reason. (I'm sorry I'm also new to Objective C, so my following explanation may be not that accurate and professional ... My previous background is mainly web development.)

If you set a breakpoint just after the line you call

testViewController *viewController = segue.destinationViewController;

when you build and run the project, you will find that the UITextField property in the destinationViewController is not allocated and initiated (memory is 0x0) at the breakpoint. Meanwhile the NSString property is already allocated and initialised (so you can set its value).

I think probably UITextfield is a child view, so it only initiates when its parent view (the destination view) is initiated. But NSString is a property that is not associated with any view, so it is allocated and initiated with the view controller which declares it.

When I do a further test of this, I find a very interesting thing: the second view is loaded during the - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender runs. I do a test code like below:

In the first view controller .m file:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    NSLog(@"1. %@, %@",[segue identifier],segue.destinationViewController);

    Scene2Controller *scene2ViewController = [segue destinationViewController];
    [txtScene2 resignFirstResponder];

    NSLog(@"2. scene2ViewController: %@", scene2ViewController);
    NSLog(@"3. txtScene1: %@; passValue: %@", [scene2ViewController txtScene1], scene2ViewController.passValue);
    NSLog(@"4. View2: %@; passValue: %@", [scene2ViewController view], scene2ViewController.passValue);
    NSLog(@"5. txtScene1: %@; passValue: %@", [scene2ViewController txtScene1], scene2ViewController.passValue);

    //txtScene1 is the UITextfield property I declared in the second view controller
    //txtScene2 is the UITextfield property I declared in the first view controller
    //passValue is the NSString property I declared in the second view controller 

}

In the second view controller .m file:

- (void)viewDidLoad
{
    NSLog(@"6. txtScene1: %@; passValue: %@", txtScene1,passValue);


    [super viewDidLoad];

}

Noticed that I add sequential numbers before NSLog messages. I found the final log result sequence was 1,2,3,6,4,5 rather than 1,2,3,4,5,6. And in log 3, the txtScene1 result was null (not initiated), but after log 4 (the second view was loaded), in log 5, the txtScene1 was NOT null and has been initiated. This suggested that the second view was loaded during the segue performs. So I guess that during the segue transition, the initiation sequence of the second view controller objects would be: second view controller -> NSString property (and other similar properties, like NSInteger, etc.) -> second view -> UITextfield property (and other subview properties).

So I changed my codes in the first view controller .m file as below:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {

    Scene2Controller *scene2ViewController = [segue destinationViewController];
    [txtScene2 resignFirstResponder];

    if ([scene2ViewController view]) {
        if ([txtScene2.text isEqualToString:@""]) {
            scene2ViewController.txtScene1.text = @"No Value";
        } else {
            scene2ViewController.txtScene1.text = txtScene2.text;
        }
    }

}

This code then works fine and the value is passed directly to the UITextfield property in the second view.

Hope above explanation is clear and helpful for you.


The viewDidLoad method from the destination controller does exactly what it says it does. It loads all the view components. So any attempts to modify them before that will be lost.

Best way to pass data is to store it in property that are not loaded during viewDidLoad but at initialisation of the object.

Typically in the FirstViewController.m:

-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
 if ([[segue identifier] isEqualToString:@"segueName"]) {
     SecondViewController *destinationVC = [segue destinationViewController];
     destinationVC.test = @"Test string";
 } }

And in the SecondViewController.m file

 -(void)viewDidLoad {
 [super viewDidLoad];
 // Do any additional setup after loading the view.
 self.testLabel.text = self.test; 
}