iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) (31 page)

Read iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) Online

Authors: Aaron Hillegass,Joe Conway

Tags: #COM051370, #Big Nerd Ranch Guides, #iPhone / iPad Programming

BOOK: iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides)
5.7Mb size Format: txt, pdf, ePub
View Controller Lifecycle

A
UIViewController
begins its life when it is allocated and sent an initializer message. A view controller will see its view created, moved on screen, moved off screen, destroyed, and created again – perhaps many times over. These events make up the view controller lifecycle.

 
Initializing view controllers

The designated initializer of
UIViewController
is
initWithNibName:bundle:
. This method takes two arguments that specify the name of the view controller’s XIB file inside a bundle. (Remember that building a target in
Xcode
creates an application bundle; this is the same kind of bundle we’re talking about here.)

 

Passing
nil
for both arguments says,

When you load your view, search for a XIB file whose name is the same as your class and search inside the application bundle.

For example,
TimeViewController
will load the
TimeViewController.xib
file. You can be more explicit about it; add the following code to
TimeViewController.m
.

 
- (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle
{
    
self = [super initWithNibName:nil
                          
bundle:nil];
    
    // Get a pointer to the application bundle object
    NSBundle *appBundle = [NSBundle mainBundle];
    self = [super initWithNibName:@"TimeViewController"
                           bundle:appBundle];
    if (self) {
        UITabBarItem *tbi = [self tabBarItem];
        [tbi setTitle:@"Time"];
        UIImage *i = [UIImage imageNamed:@"Time.png"];
        [tbi setImage:i];
    }
    return self;
}
 

In practice, iOS programmers use the name of the
UIViewController
subclass as the name of the XIB file. Thus, when creating view controllers, you need only send
init
. This is equivalent to passing
nil
for both arguments of
initWithNibName:bundle:
.

 
TimeViewController *tvc = [[TimeViewController alloc] init];
// same as:
TimeViewController *tvc = [[TimeViewController alloc] initWithNibName:nil
                                                               bundle:nil];

You can override
initWithNibName:bundle:
to perform some extra initialization steps if you need them.

 

Note that the view controller’s
view
does not appear anywhere in the view controller’s initializer. Nothing that has to do with the
view
happens here. We’ll see why in the next section.

 
UIViewController and lazy loading

When a
UIViewController
is instantiated, it doesn’t create or load its
view
right away. Only when the view is moving to the screen will a view controller bother to create its
view
. By loading views only when needed, the application doesn’t take up memory that it doesn’t need to.

 

Every
UIViewController
implements the method
viewDidLoad
that gets executed right after it loads its
view
. In
HypnosisViewController.m
, override this method to log a message to the console.

 
- (void)viewDidLoad
{
    // Always call the super implementation of viewDidLoad
    [super viewDidLoad];
    NSLog(@"HypnosisViewController loaded its view.");
}
 

Then, in
TimeViewController.m
, override the same method.

 
- (void)viewDidLoad
{
    [super viewDidLoad];
    NSLog(@"TimeViewController loaded its view.");
}
 

Build and run the application. Notice that the console reports that
HypnosisViewController
loaded its
view
right away. Tap on
TimeViewController
’s tab – the console will report that its
view
is now loaded. At this point, both views have been loaded, so switching between the tabs now will no longer trigger the
viewDidLoad
method. (Try it and see.)

 

A view controller will destroy its
view
if the system is running low on memory and if its
view
isn’t currently on the screen. Run
HypnoTime
in the simulator. Select the
Time
tab so that both views are loaded.

 

Now, in the simulator, select
Simulate Memory Warning
from the
Hardware
menu. This simulates what happens when the operating system is running low on memory and tells your application to clean up stuff it isn’t using.

 

Switch back to the
Hypnosis
tab; notice that the console reports that
HypnosisViewController
loaded its
view
again. Now switch to the
Time
tab and notice that
TimeViewController
did
not
reload its view.

 

TimeViewController
’s
view
was on the screen when the memory warning occurred, so its view was not destroyed. (Destroying a view that is on the screen would result in a miserable user experience.)
HypnosisViewController
’s
view
, on the other hand, was
not
on the screen during the memory warning, so it was destroyed. When you returned to that view, it was recreated and reloaded.

 

When the
Hypnosis
tab bar item was tapped, the
UITabBarController
asked the
HypnosisViewController
for its
view
so it could add it as a subview of its own
view
.
HypnosisViewController
, like all view controllers, automatically calls
loadView
if it is sent the message
view
and does not yet have its
view
. The implementation of
UIViewController
’s
view
method looks something like this:

 
- (UIView *)view
{
    if ([self isViewLoaded] == NO)
    {
        // If I don't currently have a view, then create it
        [self loadView];
        [self viewDidLoad];
    }
    // The view is definitely going to exist here, so return it
    return view;
}
 

UIViewController
s that load their view from a XIB file follow the same behavior. In fact, the default implementation of
loadView
loads the XIB file specified by
initWithNibName:bundle:
. So when view controllers that load their views programmatically override
loadView
, they intentionally don’t call the superclass’s implementation. If they did, it would kick off a search for a XIB file.

 

Because a view controller’s view can be destroyed and reloaded, you need to take some precautions when writing code that initializes a view controller. Let’s do something wrong to see the potential problem.

 

In
TimeViewController.m
, set the background color of
TimeViewController
’s
view
in
initWithNibName:bundle:
.

 
- (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle
{
    self = [super initWithNibName:@"TimeViewController"
                           bundle:[NSBundle mainBundle]];
    if (self) {
        UITabBarItem *tbi = [self tabBarItem];
        [tbi setTitle:@"Time"];
        UIImage *i = [UIImage imageNamed:@"Time.png"];
        [tbi setImage:i];
        
[[self view] setBackgroundColor:[UIColor greenColor]];
    }
    return self;
}

Build and run the application. The first thing you will notice is that the console reports that
HypnosisViewController
immediately loads its
view
. Switch to the
Time
tab, and note that the background color is green. Then switch back to the
Hypnosis
tab and simulate a memory warning.

 

Now switch back to the
Time
tab, and you’ll see that the background color is no longer green. The view controller reloaded its
view
, but the view controller’s initializer was not called again. So, the line of code that set the background color of its
view
to green was not executed, and the view in question was not configured correctly when the view was reloaded.

 

This is an important lesson: while you can do initial set-up for a view controller in
initWithNibName:bundle:
, you should never access a view controller’s
view
or any view in its view hierarchy in that method. Instead,
viewDidLoad
is where you should put any extra set-up messages you wish to send to a view controller’s
view
or any of its subviews.

 

In
TimeViewController.m
, move the line of code that changes the background color from
initWithNibName:bundle:
to
viewDidLoad
.

 
- (id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)bundle
{
    self = [super initWithNibName:@"TimeViewController"
                           bundle:[NSBundle mainBundle]];
    if (self) {
        UITabBarItem *tbi = [self tabBarItem];
        [tbi setTitle:@"Time"];
        UIImage *i = [UIImage imageNamed:@"Time.png"];
        [tbi setImage:i];
        
[[self view] setBackgroundColor:[UIColor greenColor]];
    }
    return self;
}
- (void)viewDidLoad
{
    [super viewDidLoad];
    NSLog(@"TimeViewController loaded its view.");
    
[[self view] setBackgroundColor:[UIColor greenColor]];
}

Build and run the application. Switch between tabs and simulate memory warnings. No matter what you do, the background color of
TimeViewController
’s
view
will be green.

 
Unloading views

If
TimeViewController
destroys its
view
during a memory warning, the
view
’s subviews will no longer be able to appear on screen and should be destroyed, too. This is what will happen with the
UIButton
; it is only owned by the
view
, so when the
view
is destroyed, the button is destroyed. However, the
UILabel
will continue to exist because it is still pointed to by the
timeLabel
instance variable of
TimeViewController
. (
Figure 7.17
).

 

Figure 7.17  TimeViewController before and after memory warning

 

When
TimeViewController
recreates its
view
, it will create a new instance of
UILabel
and set its
timeLabel
instance variable to point to it. At this point, the old label will be destroyed. But we shouldn’t wait on
TimeViewController
to reload its
view
to fix this memory leak. We can’t be sure when or even if that will happen.

 

After a view controller’s view is unloaded during a memory warning, the view controller is sent the message
viewDidUnload
. You override this method to get rid of any strong references to subviews of the view controller’s
view
. In
TimeViewController.m
, override
viewDidUnload
.

 
- (void)viewDidUnload
{
    [super viewDidUnload];
    NSLog(@"Unloading TimeViewController's subviews");
    timeLabel = nil;
}

Build and run this application. Tap on the
Time
tab and then go back to the
Hypnosis
tab. Simulate a memory warning and note the log message in the console. Notice that if you simulate a memory warning while
TimeViewController
’s
view
is visible, you will not see this message – a view is only unloaded if it is not on the screen.

 

Overriding
viewDidUnload
is one way to fix this problem, but we can solve it more simply with weak references. If you specify that
timeLabel
is a weak reference, then the
view
would keep the only strong reference to the
UILabel
, and when the
view
was destroyed, the
UILabel
would be destroyed, and
timeLabel
would automatically be set to
nil
.

 

Figure 7.18  TimeViewController before and after memory warning (with weak reference)

 

In
TimeViewController.h
, declare that
timeLabel
is a weak reference.

 
@interface TimeViewController : UIViewController
{
    
__weak
 IBOutlet UILabel *timeLabel;
}
 

Then, change
viewDidUnload
in
TimeViewController.m
so that it confirms that
timeLabel
is
nil
as soon as the view is unloaded.

 
- (void)viewDidUnload
{
    [super viewDidUnload];
    
NSLog(@"Unloading TimeViewController's subviews");
    
timeLabel = nil;
    
NSLog(@"timeLabel = %@", timeLabel);
}

Build and run the application. Make
TimeViewController
unload its
view
by simulating a memory warning while its view is not visible. Notice that
timeLabel
is
nil
by the time
viewDidUnload
runs.

 

The situation of a view controller having two references to a view is common. To avoid memory leaks, the convention for
IBOutlet
s is to declare them as weak references. There is one exception to this rule: you must keep a strong reference to top-level objects in a XIB file. A top-level object in a XIB file sits in the top-level of the of the
Objects
section in the outline view; you don’t have to click the disclosure tab next to another object to see it. For example, a view controller’s
view
is a top-level object, but its subviews are not (
Figure 7.19
).

 

Figure 7.19  XIB top level objects

 

The view controller, then, has ownership of its
view
, which owns all of its
subviews
. All of the objects continue to exist as long as you need them to. Therefore, you do not need to implement
viewDidUnload
to release subviews of a view controller’s view. In
TimeViewController.m
, you can delete the implementation of this method.

 
- (void)viewDidUnload
{
    
[super viewDidUnload];
    
NSLog(@"timeLabel = %@", timeLabel);
}
 

Other books

Time Lord by Clark Blaise
Rival Demons by Sarra Cannon
Beyond the Past by Carly Fall
Shadowshift by Peter Giglio
Secret of the Wolf by Susan Krinard
Before the Dawn by Kristal Lim
Building From Ashes by Elizabeth Hunter
Pachucha tirando a mal by Alfonso Ussia