Properties
Each time we’ve added an instance variable to BNRItem, we’ve declared and implemented a pair of accessor methods. Now we’re going to see how to use properties instead. Properties are a convenient alternative to writing out accessors for instance variables – one that saves a lot of typing and makes your class files much clearer to read.
Declaring properties
A property is declared in the interface of a class where methods are declared. A property declaration has the following form:
@property NSString *itemName;
When you declare a property, you are implicitly declaring a setter and a getter for the instance variable of the same name. So the above line of code is equivalent to the following:
- (void)setItemName:(NSString *)str; - (NSString *)itemName;
Each property has a set of attributes that describe the behavior of the accessor methods. The attributes are declared in parentheses after the @property directive. Here is an example:
@property (nonatomic, readwrite, strong) NSString *itemName;
There are three property attributes. Each attribute has two or three options, one of which is the default and does not have to explicitly declared.
The first attribute of a property has two options: nonatomic or atomic. This attribute has to do with multi-threaded applications and is outside the scope of this book. Most Objective-C programmers typically use nonatomic: we do at Big Nerd Ranch, and so does Apple. In this book, we’ll use nonatomic for all properties.
Let’s change BNRItem to use properties instead of accessor methods. In BNRItem.h, replace all of your accessor methods with properties that are nonatomic.
- (id)initWithItemName:(NSString *)name valueInDollars:(int)value serialNumber:(NSString *)sNumber;- (void)setItemName:(NSString *)str; - (NSString *)itemName; - (void)setSerialNumber:(NSString *)str; - (NSString *)serialNumber; - (void)setValueInDollars:(int)i; - (int)valueInDollars; - (NSDate *)dateCreated; - (void)setContainedItem:(BNRItem *)i; - (BNRItem *)containedItem; - (void)setContainer:(BNRItem *)i; - (BNRItem *)container;@property (nonatomic) BNRItem *containedItem; @property (nonatomic) BNRItem *container; @property (nonatomic) NSString *itemName; @property (nonatomic) NSString *serialNumber; @property (nonatomic) int valueInDollars; @property (nonatomic) NSDate *dateCreated; @end
Unfortunately, nonatomic is not the default option, so you will always need to explicitly declare your properties to be nonatomic.
The second attribute of a property is either readwrite or readonly. A readwrite property declares both a setter and getter, and a readonly property just declares a getter. The default option for this attribute is readwrite. This is what we want for all of BNRItem’s properties with the exception of dateCreated, which should be readonly. In BNRItem.h, declare dateCreated as a readonly property so that no setter method is declared for this instance variable.
@property (nonatomic, readonly) NSDate *dateCreated;
The final attribute of a property describe its memory management. The most common options tell us whether the accessed instance variable has a strong or weak reference to the object it points to. The default option is assign, which is for properties like valueInDollars that do not point to an object. The rest of BNRItem’s instance variables are all strong references to objects with the exception of container, which is weak. Add the strong attribute option to the appropriate properties and weak to the container property.
@property (nonatomic, strong) BNRItem *containedItem; @property (nonatomic, weak) BNRItem *container; @property (nonatomic, strong) NSString *itemName; @property (nonatomic, strong) NSString *serialNumber; @property (nonatomic) int valueInDollars; @property (nonatomic, readonly, strong) NSDate *dateCreated;
Build and run the application. You should see the exact same behavior as the last time you ran it. The only difference is that BNRItem.h is much cleaner.
Synthesizing properties
In addition to using a property to declare accessor methods, you can synthesize a property to generate the code for the accessor methods in the implementation file. Right now, BNRItem.m defines the accessor methods declared by each property. For example, the property itemName declares two accessor methods, itemName and setItemName:, and these are defined in BNRItem.m like so:
- (void)setItemName:(NSString *)str { itemName = str; } - (NSString *)itemName { return itemName; }
When you synthesize a property, you don’t have to type out the accessor definitions. You can synthesize a property by using the @synthesize directive in the implementation file. In BNRItem.m, add a synthesize statement for itemName and delete the implementations of setItemName: and itemName.
@implementation BNRItem @synthesize itemName;- (void)setItemName:(NSString *)str { itemName = str; } - (NSString *)itemName { return itemName; }
You can synthesize properties in the same synthesize statement or split them up into multiple statements. In BNRItem.m, synthesize the rest of the instance variables and delete the rest of the accessor implementations.
@implementation @synthesize itemName; @synthesize containedItem, container, serialNumber, valueInDollars, dateCreated;- (void)setSerialNumber:(NSString *)str { serialNumber = str; } - (NSString *)serialNumber { return serialNumber; } - (void)setValueInDollars:(int)i { valueInDollars = i; } - (int)valueInDollars { return valueInDollars; } - (NSDate *)dateCreated { return dateCreated; } - (void)setContainedItem:(BNRItem *)i { containedItem = i; // When given an item to contain, the contained // item will be given a pointer to its container [i setContainer:self]; } - (BNRItem *)containedItem { return containedItem; } - (void)setContainer:(BNRItem *)i { container = i; } - (BNRItem *)container { return container; }
Usually, synthesized accessors work fine, but sometimes you need an accessor method to do some additional work. This is the case for setContainedItem:. Here is our original implementation:
- (void)setContainedItem:(BNRItem *)i { containedItem = i; [i setContainer:self]; }
The synthesized setter won’t include the second line establishing the reciprocal relationship between the container and the containedItem. Its implementation just looks like this:
- (void)setContainedItem:(BNRItem *)i { containedItem = i; }
Because we need this setter to do additional work, we cannot rely on the synthesized method and must write the implementation ourselves. Fortunately, writing our own implementation does not conflict with synthesizing the property. Any implementation we add will override the synthesized version. In BNRItem.m, add back the implementation of setContainedItem:.
- (void)setContainedItem:(BNRItem *)i { containedItem = i; [i setContainer:self]; }
Build and run the application again. It should work the same as always, but your code is much cleaner.
Synthesizing a property that you declared in the header file is optional, but typical. The only reason not to synthesize a property is if both the getter and the setter methods have additional behavior you need to implement.
Instance variables and properties
With properties, we can go even one step further in code clarity. By default, a synthesized property will access the instance variable of the same name. For example, the itemName property accesses the itemName instance variable: the itemName method returns the value of the itemName instance variable, and the setItemName: method changes the itemName instance variable.
If there is no instance variable that matches the name of a synthesized property, one is automatically created. So declaring an instance variable and synthesizing a property is redundant. In BNRItem.h, remove all of the instance variables as well as the curly brackets.
@interface BNRItem : NSObject{ NSString *itemName; NSString *serialNumber; int valueInDollars; NSDate *dateCreated;BNRItem *containedItem; __weak BNRItem *container; }
Build and run the application. Notice there are no errors and everything works fine. All of the instance variables (like itemName and dateCreated) still exist even though we no longer explicitly declare them.