In part 1 of this blog, we created an app that has a single view controller that dynamically creates two subviews with a single button each. We used delegation through a protocol to respond to the button touches, but the view controller isn’t responding to the delegate methods. The source code for part 1 is available here:[link to part 1 source code here].
In order to diagnose the problem, let’s review how we’re setting up the view controller. In initWithNibName: bundle, we instantiate the two views and make them subviews of the view controller’s view:
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
CGRect view1Frame = CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height / 2);
self.view1 = [[View1 alloc] initWithFrame:view1Frame];
self.view1.backgroundColor = [UIColor redColor];
[self.view addSubview:self.view1];
CGRect view2Frame = CGRectMake(0, self.view.bounds.size.height / 2, self.view.bounds.size.width, self.view.bounds.size.height);
self.view2 = [[View2 alloc] initWithFrame:view2Frame];
self.view2.backgroundColor = [UIColor blueColor];
[self.view addSubview:self.view2];
}
return self;
}
We know this code works, because the views display properly when the app is run:
Image may be NSFW.
Clik here to view.
In viewDidLoad, we set self (this ViewController) to be the delegate of each view:
{
[super viewDidLoad];
self.view1.delegate = self;
self.view2.delegate = self;
}
This seems reasonable, since the view controller’s init method should be done by the time the view loads. But wait… what view is referred to in viewDidLoad? View1? View2? Some other view?
The correct answer is “some other view.” All view controller’s have a view. This view controller’s view is the one declared in ViewController.xib (which is empty). ViewDidLoad is a delegate method: it will be called when the ViewController’s main view is loaded, regardless of whether initWithNibName: bundle: is done. In this case, viewDidLoad is attempting to set self to the delegate property of each view before the views have been instantiated! Since view1 and view2 are uninitialized, the code in the view controller is equivalent to saying:
{
[super viewDidLoad];
nil = self;
nil = self;
}
(not really, but this gives a good mental approximation, since uninitialized object properties are basically nil.)
So how do we fix this? In this case, the solution is simple: move the delegate setting code from viewDidLoad to initWithNibName: bundle:
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
CGRect view1Frame = CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height / 2);
self.view1 = [[View1 alloc] initWithFrame:view1Frame];
self.view1.backgroundColor = [UIColor redColor];
[self.view addSubview:self.view1];
CGRect view2Frame = CGRectMake(0, self.view.bounds.size.height / 2, self.view.bounds.size.width, self.view.bounds.size.height);
self.view2 = [[View2 alloc] initWithFrame:view2Frame];
self.view2.backgroundColor = [UIColor blueColor];
[self.view addSubview:self.view2];
self.view1.delegate = self;
self.view2.delegate = self;
}
return self;
}
Now run the app, click the buttons, and note the result:
Image may be NSFW.
Clik here to view.
The first message from each button is from the action method in each view, the second is from the view controller’s implementation of the delegate method.
Alternatively, we could move all code inside initWithNibName: bundle: to viewDidLoad, and do away with the override of initWithNibName: bundle: altogether. The point is that we should always ensure that objects are properly allocated and initialized before attempting to set or get properties of those objects.
Perhaps the best way to ensure that objects are created before being used is “lazy instantiation.” In this approach, we override the getters for properties to alloc/init them if they do not already exist. Here is a new ViewController.m that dispenses with initWithNibName: bundle: in favor of lazily instantiating view1 and view2:
@interface ViewController ()
@end
@implementation ViewController
@synthesize view1, view2;
- (View1 *)view1
{
if (!view1) {
CGRect view1Frame = CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height / 2);
self.view1 = [[View1 alloc] initWithFrame:view1Frame];
self.view1.backgroundColor = [UIColor redColor];
[self.view addSubview:self.view1];
}
return view1;
}
- (View2 *)view2
{
if (!view2) {
CGRect view2Frame = CGRectMake(0, self.view.bounds.size.height / 2, self.view.bounds.size.width, self.view.bounds.size.height);
self.view2 = [[View2 alloc] initWithFrame:view2Frame];
self.view2.backgroundColor = [UIColor blueColor];
[self.view addSubview:self.view2];
}
return view2;
}
- (void)view1ButtonPressed
{
NSLog(@"Button 1 Touched");
}
- (void)view2ButtonPressed
{
NSLog(@"Button 2 Touched");
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.view1.delegate = self;
self.view2.delegate = self;
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
view1 = nil;
view2 = nil;
}
@end
When viewDidLoad binds the delegates to self, the getters are called in the view objects, and the app now runs properly: