Search code examples
iosobjective-cpropertiesweak-references

Setting a non-nil object to a weak property returns nil second time but not first one


I'm working on a legacy app which I'm refactoring, and there is a weird behavior I don't understand.

There is a UIViewController called CANoContentViewController and it has a simple initialization code from a Nib:

+ (CANoContentViewController *)instantiateController {
   CANoContentViewController *vc = [[CANoContentViewController alloc] initWithNibName:@"CANoContentViewController" bundle:[NSBundle mainBundle]];
   DLog(@"Created CANoContentViewController %@", vc);
   return vc;
}

Then, other UIViewController shows it if there is no content to show. Here is the code:

@property (nonatomic, weak) UIViewController *noContentViewController;

-(void)showOrDeleteNoContentIfNeeded{
   if([self.proposals count] <= 0) { // Show No Content VC
       self.noContentViewController = [CANoContentViewController instantiateController];
       DLog(@"Set CANoContentViewController %@", self.noContentViewController);
       self.noContentViewController.view.frame = self.view.bounds;
       [self addChildViewController:self.noContentViewController];
       [self.view addSubview:self.noContentViewController.view];
       [self.tableView setSeparatorStyle:UITableViewCellSeparatorStyleNone];
   } else {
       [self.noContentViewController.view removeFromSuperview];
       [self.noContentViewController removeFromParentViewController];
       [self.tableView setSeparatorStyle:UITableViewCellSeparatorStyleSingleLine];
   }
}

The problem here is that first time CANoContentViewController is shown, all works fine, and the logs are as expected:

2018-06-12 00:58:29.303276+0200 Base[11828:838463] +[CANoContentViewController instantiateController](0x105cc6688) Created CANoContentViewController <CANoContentViewController: 0x7fc350e101f0>
2018-06-12 00:58:29.303517+0200 Base[11828:838463] -[CAProposalsViewController showOrDeleteNoContentIfNeeded](0x7fc35106b200) Set CANoContentViewController <CANoContentViewController: 0x7fc350e101f0>

As you can see, the created controller is set correctly in the property.

The second time this code is executed, even being a new instance of the parent view controller which will instantiate CANoContentViewController the CANoContentViewController is created correctly as logs shows, but it's not set to the property:

2018-06-12 00:58:31.379708+0200 Base[11828:838463] +[CANoContentViewController instantiateController](0x105cc6688) Created CANoContentViewController <CANoContentViewController: 0x7fc350c8bca0>
2018-06-12 00:58:31.380275+0200 Base[11828:838463] -[CAProposalsViewController showOrDeleteNoContentIfNeeded](0x7fc35181f200) Set CANoContentViewController (null)

So the app crashes because it's trying to set a nil object in method addChildViewController.

Anyone knowns why second time this line doesn't works and the property is nil?

self.noContentViewController = [CANoContentViewController instantiateController];

I've checked that when I change the property to strong all works great. But I can't understand why is this happening because every time the CANoContentViewController is created from a new parent view controller which self.noContentViewController property should be different to the previous one.


Solution

  • After your execute:

    self.noContentViewController = [CANoContentViewController instantiateController];
    

    there is no longer any strong reference to the object you created and assigned to the weak property so it gets deallocated and the weak property becomes nil. The fact it works the first time is just luck and can't be counted on.

    If you want the instance to survive long enough to work on it in that method, assign to a local variable.

    -(void)showOrDeleteNoContentIfNeeded{
       if([self.proposals count] <= 0) { // Show No Content VC
           CANoContentViewController *controller = [CANoContentViewController instantiateController];
           self.noContentViewController = controller;
           DLog(@"Set CANoContentViewController %@", self.noContentViewController);
           self.noContentViewController.view.frame = self.view.bounds;
           [self addChildViewController:self.noContentViewController];
           [self.view addSubview:self.noContentViewController.view];
           [self.tableView setSeparatorStyle:UITableViewCellSeparatorStyleNone];
       } else {
           [self.noContentViewController.view removeFromSuperview];
           [self.noContentViewController removeFromParentViewController];
           [self.tableView setSeparatorStyle:UITableViewCellSeparatorStyleSingleLine];
       }
    }
    

    The use of the local variable will keep a strong reference during the scope of the local variable. This holds the reference long enough to call addChildViewController which creates another strong reference.