iOS Programming: The Big Nerd Ranch Guide, 3/e (Big Nerd Ranch Guides) (12 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)
4.93Mb size Format: txt, pdf, ePub
Accessor methods

Now that you have instance variables, you need a way to get and set their values. In object-oriented languages, we call methods that get and set instance variables
accessors
. Individually, we call them
getters
and
setters
. Without these methods, an object cannot access the instance variables of another object.

 

Accessor methods look like this:

 
// a getter method
- (NSString *)itemName
{
    // Return a pointer to the object this BNRItem calls its itemName
    return itemName;
}
// a setter method
- (void)setItemName:(NSString *)newItemName
{
    // Change the instance variable to point at another string,
    // this BNRItem will now call this new string its itemName
    itemName = newItemName;
}
 

Then, if you wanted to access (set or get) a
BNRItem
’s
itemName
, you would send the
BNRItem
one of these messages:

 
// Create a new BNRItem instance
BNRItem *p = [[BNRItem alloc] init];
// Set itemName to a new NSString
[p setItemName:@"Red Sofa"];
// Get the pointer of the BNRItem's itemName
NSString *str = [p itemName];
// Print that object
NSLog(@"%@", str); // This would print "Red Sofa"
 

In Objective-C, the name of a setter method is
set
plus the name of the instance variable it is changing – in this case,
setItemName:
. In other languages, the name of the getter method would likely be
getItemName
. However, in Objective-C, the name of the getter method is just the name of the instance variable. Some of the cooler parts of the Cocoa Touch library make the assumption that your classes follow this convention; therefore, stylish Cocoa Touch programmers always do so.

 

In
BNRItem.h
, declare accessor methods for the instance variables of
BNRItem
. You will need getters and setters for
valueInDollars
,
itemName
, and
serialNumber
. For
dateCreated
, you only need a getter method.

 
#import
@interface BNRItem : NSObject
{
    NSString *itemName;
    NSString *serialNumber;
    int valueInDollars;
    NSDate *dateCreated;
}
- (void)setItemName:(NSString *)str;
- (NSString *)itemName;
- (void)setSerialNumber:(NSString *)str;
- (NSString *)serialNumber;
- (void)setValueInDollars:(int)i;
- (int)valueInDollars;
- (NSDate *)dateCreated;
@end

(For those of you with some experience in Objective-C, we’ll talk about properties in the next chapter.)

 

Now that these accessors have been declared, they need to be defined in the implementation file. Select
BNRItem.m
in the project navigator to open it in the editor area.

 

At the top of any implementation file, you must import the header file of that class. The implementation of a class needs to know how it has been declared. (Importing a file is the same as including a file in the C language except you are ensured that the file will only be included once.)

 

After the import statement is the implementation block that begins with the
@implementation
keyword followed by the name of the class that is being implemented. All of the method definitions in the implementation file are inside this implementation block. Methods are defined until you close out the block with the
@end
keyword.

 

When you created this class, the template you used may have inserted methods for you. However, we want to start from scratch. Using boilerplate methods before you understand what they do keeps you from learning how things actually work. In
BNRItem.m
, delete everything that the template may have added between
@implementation
and
@end
. Your file should look like this:

 
#import "BNRItem.h"
@implementation BNRItem
@end
 

Now we can define some methods of our own – starting with the accessor methods for the variables you declared in
BNRItem.h
. We’re going to skip memory management until the next chapter, so the accessor methods for
BNRItem
are very simple. In
BNRItem.m
, add the following code.

 
#import "BNRItem.h"
@implementation BNRItem
- (void)setItemName:(NSString *)str
{
    itemName = str;
}
- (NSString *)itemName
{
    return itemName;
}
- (void)setSerialNumber:(NSString *)str
{
    serialNumber = str;
}
- (NSString *)serialNumber
{
    return serialNumber;
}
- (void)setValueInDollars:(int)i
{
    valueInDollars = i;
}
- (int)valueInDollars
{
    return valueInDollars;
}
- (NSDate *)dateCreated
{
    return dateCreated;
}
@end
 

Notice that the setter methods assign the appropriate instance variable to point at the incoming object, and the getter methods return a pointer to the object the instance variable points at. (For
valueInDollars
, the setter just assigns the passed-in value to the instance variable, and the getter just returns the instance variable’s value.)

 

Build your application (without running) to ensure that there are no compiler errors or warnings. To build only, select
Product

Build
or use the shortcut Command-B.

 

Now that your accessors have been declared and defined, you can send messages to
BNRItem
instances to get and set their instance variables. Let’s test this out. In
main.m
, import the header file for
BNRItem
and create a new
BNRItem
instance. After it is created, log its instance variables to the console.

 
#import "BNRItem.h"
int main (int argc, const char * argv[])
{
    @autoreleasepool {
        NSMutableArray *items = [[NSMutableArray alloc] init];
        [items addObject:@"One"];
        [items addObject:@"Two"];
        [items addObject:@"Three"];
        [items insertObject:@"Zero" atIndex:0];
        for (int i = 0; i < [items count]; i++) {
            NSLog(@"%@", [items objectAtIndex:i]);
        }
        BNRItem *p = [[BNRItem alloc] init];
        NSLog(@"%@ %@ %@ %d", [p itemName], [p dateCreated],
                              [p serialNumber], [p valueInDollars]);
        
        items = nil;
    }
    return 0;
}
 

Build and run the application. Check the console by selecting the entry at the top of the log navigator. At the end of the console output, you should see a line that has three

(null)

strings and a 0. When an object is created, all of its instance variables are set to 0. For pointers to objects, that pointer points to
nil
; for primitives like
int
, the value is 0.

 

To give this
BNRItem
some substance, you need to create new objects and pass them as arguments to the setter methods for this instance. In
main.m
, type in the following code:

 
// Notice we omitted some of the surrounding code. The bold code is the code to add,
// the non-bold code is existing code that shows you where to type in the new stuff.
BNRItem *p = [[BNRItem alloc] init];
// This creates a new NSString, "Red Sofa" and gives it to the BNRItem
[p setItemName:@"Red Sofa"];
// This creates a new NSString, "A1B2C" and gives it to the BNRItem
[p setSerialNumber:@"A1B2C"];
// We send the value 100 to be used as the valueInDollars of this BNRItem
[p setValueInDollars:100];
NSLog(@"%@ %@ %@ %d", [p itemName], [p dateCreated],
                      [p serialNumber], [p valueInDollars]);

Build and run the application. Now you should see values for everything but the
dateCreated
, which we’ll take care of shortly.

 
Instance methods

Not all instance methods are accessors. You will regularly find yourself wanting to send messages to instances that perform other tasks. One such message is
description
. You can implement this method in
BNRItem
to return a string that describes a
BNRItem
instance. Because
BNRItem
is a subclass of
NSObject
(the class that originally declares the
description
method), when you re-implement
description
in the
BNRItem
class, you are
overriding
it. When overriding a method, all you need to do is define it in the implementation file; you do not need to declare it in the header file because it has already been declared by the superclass.

 

In
BNRItem.m
, override the
description
method. This new code can go anywhere between
@implementation
and
@end
, as long as it is not inside the curly brackets of an existing method.

 
- (NSString *)description
{
    NSString *descriptionString =
        [[NSString alloc] initWithFormat:@"%@ (%@): Worth $%d, recorded on %@",
                            itemName,
                            serialNumber,
                            valueInDollars,
                            dateCreated];
    return descriptionString;
}
 

Now whenever you send the message
description
to an instance of
BNRItem
, it will return an
NSString
that describes that instance. In
main.m
, substitute this new method into the
NSLog
that prints out the instance variables of the
BNRItem
.

 
[p setValueInDollars:100];
NSLog(@"%@ %@ %@ %d", [p itemName], [p dateCreated],
                      
[p serialNumber], [p valueInDollars]);
// Remember, an NSLog with %@ as the token will print the
// description of the corresponding argument
NSLog(@"%@", p);
items = nil;

Build and run the application and check your results in the log navigator. You should see a log statement that looks like this:

 
Red Sofa (A1B2C): Worth $100, recorded on (null)
 

What if you want to create an entirely new instance method, one that you are not overriding from the superclass? You declare the new method in the header file and define it in the implementation file. A good method to begin with is an object’s initializer.

 
Initializers

At the beginning of this chapter, we discussed how an instance is created: its class is sent the message
alloc
, which creates an instance of that class and returns a pointer to it, and then that instance is sent the message
init
, which gives its instance variables initial values. As you start to write more complicated classes, you will want to create initialization methods, or
initializers
, that are like
init
but take arguments that the object can use to initialize itself. For example, the
BNRItem
class would be much cleaner if we could pass one or more of its instance variables as part of the initialization process.

 

To cover the different possible initialization scenarios, many classes have more than one initializer. Each initializer begins with the word
init
. Naming initializers this way doesn’t make these methods different from other instance methods; it is only a naming convention. However, the Objective-C community is all about naming conventions, which you should strictly adhere to. (Seriously. Disregarding naming conventions in Objective-C results in problems that are worse than most beginners would imagine.)

 

For each class, regardless of how many initialization methods there are, one method is chosen as the
designated initializer
. The designated initializer makes sure that every instance variable of an object is valid. (

Valid

has different meanings, but in this context it means

when you send messages to this object after initializing it, you can predict the outcome and nothing bad will happen.

)

 

Typically, the designated initializer has parameters for the most important and frequently used instance variables of an object. The
BNRItem
class has four instance variables, but only three are writeable. Therefore,
BNRItem
’s designated initializer should accept three arguments. In
BNRItem.h
, declare the designated initializer:

 
    NSDate *dateCreated;
}
- (id)initWithItemName:(NSString *)name
        valueInDollars:(int)value
          serialNumber:(NSString *)sNumber;
- (void)setItemName:(NSString *)str;
 

This method’s name, or selector, is
initWithItemName:valueInDollars:serialNumber:
. This selector has three labels (
initWithItemName:
,
valueInDollars:
, and
serialNumber:
), which tells you that the method accepts three arguments.

 

These arguments each have a type and a parameter name. In the declaration, the type follows the label in parentheses. The parameter name then follows the type. So the label
initWithItemName:
is expecting a pointer to an instance of type
NSString
. Within the body of this method, you can use
name
to reference the
NSString
object pointed to.

 
id

Take another look at the initializer’s declaration; its return type is
id
(pronounced

eye-dee

). This type is defined as

a pointer to any object.

(
id
is a lot like
void *
in C.)
init
methods are always declared to return
id
.

 

Why not make the return type
BNRItem *
? After all, that is the type of object that is returned from this method. A problem will arise, however, if
BNRItem
is ever subclassed. The subclass would inherit all of the methods from
BNRItem
, including this initializer and its return type. An instance of the subclass could then be sent this initializer message, but what would be returned? Not a
BNRItem
, but an instance of the subclass. You might think,

No problem. Override the initializer in the subclass to change the return type.

But in Objective-C, you cannot have two methods with the same selector and different return types (or arguments). By specifying that an initialization method returns

any object,

we never have to worry what happens with a subclass.

 
isa

As programmers, we always know the type of the object that is returned from an initializer. (How do we know this? It is an instance of the class we sent
alloc
to.) The object itself also knows its type – thanks to its
isa
pointer.

 

Every object has an instance variable called
isa
. When an instance is created by sending
alloc
to a class, that class sets the
isa
instance variable of the returned object to point back at the class that created it (
Figure 2.13
). We call it the
isa
pointer because an object

is a

instance of that class.

 

Figure 2.13  The isa pointer

 

The
isa
pointer is where Objective-C gets much of its power. At runtime, when a message is sent to an object, that object goes to the class named in its
isa
pointer and says,

I was sent this message. Run the code for the matching method.

This is different than most compiled languages, where the method to be executed is determined at compile time.

 
Implementing the designated initializer

Now that you have declared the designated initializer in
BNRItem.h
, you need to implement it. Open
BNRItem.m
. Recall that the definitions for methods go within the implementation block in the implementation file, so add the designated initializer there.

 
@implementation BNRItem
- (id)initWithItemName:(NSString *)name
        valueInDollars:(int)value
          serialNumber:(NSString *)sNumber
{
    // Call the superclass's designated initializer
    self = [super init];
    // Give the instance variables initial values
    [self setItemName:name];
    [self setSerialNumber:sNumber];
    [self setValueInDollars:value];
    dateCreated = [[NSDate alloc] init];
    // Return the address of the newly initialized object
    return self;
}
 

In the designated initializer, the first thing you always do is call the superclass’s designated initializer using
super
. The last thing you do is return a pointer to the successfully initialized object using
self
. So to understand what’s going on in an initializer, you will need to know about
self
and
super
.

 
self

Inside a method,
self
is an implicit local variable. There is no need to declare it, and it is automatically set to point to the object that was sent the message. (Most object-oriented languages have this concept, but some call it
this
instead of
self
.) Typically,
self
is used so that an object can send a message to itself:

 
- (void)chickenDance
{
    [self pretendHandsAreBeaks];
    [self flapWings];
    [self shakeTailFeathers];
}
 

In the last line of an
init
method, you always return the newly initialized object so that the caller can assign it to a variable:

 
return self;
 
super

Often when you are overriding a method, you want to keep what the method of the superclass is doing and have your subclass add something new on top of it. To make this easier, there is a compiler directive in Objective-C called
super
:

 
- (void)someMethod
{
    [self doMoreStuff];
    [super someMethod];
}
 

How does
super
work? Usually when you send a message to an object, the search for a method of that name starts in the object’s class. If there is no such method, the search continues in the superclass of the object. The search will continue up the inheritance hierarchy until a suitable method is found. (If it gets to the top of the hierarchy and no method is found, an exception is thrown.)

 

When you send a message to
super
, you are sending a message to
self
, but the search for the method skips the object’s class and starts at the superclass. In the case of
BNRItem
’s designated initializer, we send the
init
message to
super
. This calls
NSObject
’s implementation of
init
.

 

If an initializer message fails, it will return
nil
. Therefore, it is a good idea to save the return value of the superclass’s initializer into the
self
variable and confirm that it is not
nil
before doing any further initialization. In
BNRItem.m
, edit your designated initializer to confirm the initialization of the superclass.

 
- (id)initWithItemName:(NSString *)name
        valueInDollars:(int)value
          serialNumber:(NSString *)sNumber
{
    // Call the superclass's designated initializer
    self = [super init];
    // Did the superclass's designated initializer succeed?
    if (self) {
        // Give the instance variables initial values
        [self setItemName:name];
        [self setSerialNumber:sNumber];
        [self setValueInDollars:value];
        dateCreated = [[NSDate alloc] init];
    }
    // Return the address of the newly initialized object
    return self;
}
 

Other books

Blitz by Claire Rayner
Diplomatic Immunity by Grant. Sutherland
EDGE by Tiffinie Helmer
McNally's luck by Lawrence Sanders
Gertrude by Hermann Hesse
Robyn and the Hoodettes by Ebony McKenna