Creating Your Own Classes
Where I live, the state government has decided that the uneducated have entirely too much money: You can play the lottery every week. Let’s imagine that a lottery entry has two numbers between 1 and 100, inclusive. You will write a program that will make up lottery entries for the next ten weeks. Each LotteryEntry object will have a date and two random integers (Figure 3.5). Besides learning how to create classes, you will build a tool that will certainly make you fabulously wealthy.
Figure 3.5. Completed Program
Creating the LotteryEntry Class
In Xcode, create a new file. Select Objective-C class as the type. Name the class LotteryEntry, and set it to be a subclass of NSObject (Figure 3.6).
Figure 3.6. New LotteryEntry Class
Note that you are also causing LotteryEntry.h to be created. Drag both files into the lottery group if they are not already there.
LotteryEntry.h
Edit the LotteryEntry.h file to look like this:
#import <Foundation/Foundation.h> @interface LotteryEntry : NSObject { NSDate *entryDate; int firstNumber; int secondNumber; } - (void)prepareRandomNumbers; - (void)setEntryDate:(NSDate *)date; - (NSDate *)entryDate; - (int)firstNumber; - (int)secondNumber; @end
You have created a header file for a new class called LotteryEntry that inherits from NSObject. It has three instance variables:
- entryDate is an NSDate.
- firstNumber and secondNumber are both ints.
You have declared five methods in the new class:
- prepareRandomNumbers will set firstNumber and secondNumber to random values between 1 and 100. It takes no arguments and returns nothing.
- entryDate and setEntryDate: will allow other objects to read and set the variable entryDate. The method entryDate will return the value stored in the entryDate variable. The method setEntryDate: will allow the value of the entryDate variable to be set. Methods that allow variables to be read and set are called accessor methods.
- You have also declared accessor methods for reading firstNumber and secondNumber. (You have not declared accessors for setting these variables; you are going to set them directly in prepareRandomNumbers.)
LotteryEntry.m
Edit LotteryEntry.m to look like this:
#import "LotteryEntry.h" @implementation LotteryEntry - (void)prepareRandomNumbers { firstNumber = ((int)random() % 100) + 1; secondNumber = ((int)random() % 100) + 1; } - (void)setEntryDate:(NSDate *)date { entryDate = date; } - (NSDate *)entryDate { return entryDate; } - (int)firstNumber { return firstNumber; } - (int)secondNumber { return secondNumber; } @end
Here is the play-by-play for each method:
prepareRandomNumbers uses the standard random function to generate a pseudorandom number. You use the mod operator (%) and add 1 to get the number in the range 1–100.
setEntryDate: sets the pointer entryDate to a new value.
entryDate, firstNumber, and secondNumber return the values of variables.
Changing main.m
Now let’s look at main.m. Many of the lines have stayed the same, but several have changed. The most important change is that we are using LotteryEntry objects instead of NSNumber objects.
Here is the heavily commented code. (You don’t have to type in the comments.)
#import <Foundation/Foundation.h> #import "LotteryEntry.h" int main (int argc, const char *argv[]) { @autoreleasepool { // Create the date object NSDate *now = [[NSDate alloc] init]; NSCalendar *cal = [NSCalendar currentCalendar]; NSDateComponents *weekComponents = [[NSDateComponents alloc] init]; // Seed the random number generator srandom((unsigned)time(NULL)); NSMutableArray *array; array = [[NSMutableArray alloc] init]; int i; for (i = 0; i < 10; i++) { [weekComponents setWeek:i]; // Create a date/time object that is 'i' weeks from now NSDate *iWeeksFromNow; iWeeksFromNow = [cal dateByAddingComponents:weekComponents toDate:now options:0]; // Create a new instance of LotteryEntry LotteryEntry *newEntry = [[LotteryEntry alloc] init]; [newEntry prepareRandomNumbers]; [newEntry setEntryDate:iWeeksFromNow]; // Add the LotteryEntry object to the array [array addObject:newEntry]; } for (LotteryEntry *entryToPrint in array) { // Display its contents NSLog(@"%@", entryToPrint); } } return 0; }
Note the second loop. Here you are using Objective-C’s mechanism for enumerating over the members of a collection.
This program will create an array of LotteryEntry objects, as shown in Figure 3.7.
Figure 3.7. Object Diagram
Implementing a description Method
Build and run your application. You should see something like Figure 3.8.
Figure 3.8. Completed Execution
Hmm. Not quite what we hoped for. After all, the program is supposed to reveal the dates and the numbers you should play on those dates, and you can’t see either. (You are seeing the default description method as defined in NSObject.) Next, you will make the LotteryEntry objects display themselves in a more meaningful manner.
Add a description method to LotteryEntry.m:
- (NSString *)description { NSDateFormatter *df = [[NSDateFormatter alloc] init]; [df setTimeStyle:NSDateFormatterNoStyle]; [df setDateStyle:NSDateFormatterMediumStyle]; NSString *result; result = [[NSString alloc] initWithFormat:@"%@ = %d and %d", [df stringFromDate:entryDate], firstNumber, secondNumber]; return result; }
Build and run the application. Now you should see the dates and numbers:
Figure 3.9. Execution with Description
NSDate
Before moving on to any new ideas, let’s examine NSDate in some depth. Instances of NSDate represent a single point in time and are basically immutable: You can’t change the day or time once it is created. Because NSDate is immutable, many objects often share a single date object. There is seldom any need to create a copy of an NSDate object.
Here are some of the commonly used methods implemented by NSDate:
+ (id)date
Creates and returns a date initialized to the current date and time.
This is a class method. In the interface file, implementation file, and documentation, class methods are recognizable because they start with + instead of –. A class method is triggered by sending a message to the class instead of an instance. This one, for example, could be used as follows:
NSDate *now; now = [NSDate date]; - (id)dateByAddingTimeInterval:(NSTimeInterval)interval
Creates and returns a date initialized to the date represented by the receiver plus the given interval.
- (NSTimeInterval)timeIntervalSinceDate:(NSDate *)anotherDate
Returns the interval in seconds between the receiver and anotherDate. If the receiver is earlier than anotherDate, the return value is negative. NSTimeInterval is the same as double.
+ (NSTimeInterval)timeIntervalSinceReferenceDate
Returns the interval in seconds between the first instant of January 1, 2001 GMT and the receiver’s time.
- (NSComparisonResult)compare:(NSDate *)otherDate
Returns NSOrderedAscending if the receiver is earlier than otherDate, NSOrderedDescending if otherDate is earlier, or NSOrderedSame if the receiver and otherDate are equal.
Writing Initializers
Notice the following lines in your main function:
newEntry = [[LotteryEntry alloc] init]; [newEntry prepareRandomNumbers];
You are creating a new instance and then immediately calling prepareRandomNumbers to initialize firstNumber and secondNumber. This is something that should be handled by the initializer, so you are going to override the init method in your LotteryEntry class.
In the LotteryEntry.m file, change the method prepareRandomNumbers into an init method:
- (id)init { self = [super init]; if (self) { firstNumber = ((int)random() % 100) + 1; secondNumber = ((int)random() % 100) + 1; } return self; }
The init method calls the superclass’s initializer at the beginning, initializes its own variables, and then returns self, a pointer to the object itself (the object that is running this method). (If you are a Java or C++ programmer, self is the same as the this pointer.)
Now delete the following line in main.m:
[newEntry prepareRandomNumbers];
In LotteryEntry.h, delete the following declaration:
- (void)prepareRandomNumbers;
Build and run your program to reassure yourself that it still works.
Take another look at our init method. Why do we bother to assign the return value of the superclass’s initializer to self and then test the value of self? The answer is that the initializers of some Cocoa classes will return nil if initialization was impossible. In order to handle these cases gracefully, we must both test the return value of [super init] and return the appropriate value for self from our initiailizer.
This pattern is debated among some Objective-C programmers. Some say that it is unnecessary, since most classes’ initializers don’t fail, and most classes’ initializers don’t return a different value for self. We believe it best to be in the habit of assigning to self and testing that value. The effort required is minimal compared to the debugging headaches that await you if you make an incorrect assumption about the superclass’s behavior.
Initializers with Arguments
Look at the same place in main.m. It should now look like this:
LotteryEntry *newEntry = [[LotteryEntry alloc] init]; [newEntry setEntryDate:iWeeksFromNow];
It might be nicer if you could supply the date as an argument to the initializer. Change those lines to look like this:
LotteryEntry *newEntry = [[LotteryEntry alloc] initWithEntryDate:iWeeksFromNow];
You may see a compiler error; ignore it, as we are about to fix the problem.
Next, declare the method in LotteryEntry.h:
- (id)initWithEntryDate:(NSDate *)theDate;
Now, change (and rename) the init method in LotteryEntry.m:
- (id)initWithEntryDate:(NSDate *)theDate { self = [super init]; if (self) { entryDate = theDate; firstNumber = ((int)random() % 100) + 1; secondNumber = ((int)random() % 100) + 1; } return self; }
Build and run your program. It should work correctly.
However, your class LotteryEntry has a problem. You are going to e-mail the class to your friend Rex. Rex plans to use the class LotteryEntry in his program but might not realize that you have written initWithEntryDate:. If he made this mistake, he might write the following lines of code:
NSDate *today = [NSDate date]; LotteryEntry *bigWin = [[LotteryEntry alloc] init]; [bigWin setEntryDate:today];
This code will not create an error. Instead, it will simply go up the inheritance tree until it finds NSObject’s init method. The problem is that firstNumber and secondNumber will not get initialized properly—both will be zero.
To protect Rex from his own ignorance, you will override init to call your initializer with a default date:
- (id)init { return [self initWithEntryDate:[NSDate date]]; }
Add this method to your LotteryEntry.m file.
Note that initWithEntryDate: still does all the work. Because a class can have multiple initializers, we call the one that does the work the designated initializer. If a class has several initializers, the designated initializer typically takes the most arguments. You should clearly document which of your initializers is the designated initializer. Note that the designated initializer for NSObject is init.
Conventions for Creating Initializers (rules that Cocoa programmers try to follow regarding initializers):
- You do not have to create any initializer in your class if the superclass’s initializers are sufficient.
- If you decide to create an initializer, you must override the superclass’s designated initializer.
- If you create multiple initializers, only one does the work—the designated initializer. All other initializers call the designated initializer.
- The designated initializer of your class will call its superclass’s designated initializer.
The day will come when you will create a class that must, must, must have some argument supplied. Override the superclass’s designated initializer to throw an exception:
- (id)init { @throw [NSException exceptionWithName:@"BNRBadInitCall" reason:@"Initialize Lawsuit with initWithDefendant:" userInfo:nil]; return nil; }