Properties
Each time you have declared an instance variable in BNRItem, you have declared and implemented a pair of accessor methods. Now you are going to learn to use properties, a convenient alternative to writing out accessors methods that saves a lot of typing and makes your class files easier to read.
Declaring properties
A property declaration has the following form:
@property NSString *itemName;
By default, declaring a property will get you three things: an instance variable and two accessors for the instance variable. Take a look at Table 3.1, which shows a class not using properties on the left and the equivalent class with properties on the right.
Table 3.1 With and without properties
Without properties |
With properties |
|
BNRThing.h |
@interface BNRThing : NSObject { NSString *_name; } - (void)setName:(NSString *)n; - (NSString *)name; @end |
@interface BNRThing : NSObject @property NSString *name; @end |
BNRThing.m |
@implementation BNRThing - (void)setName:(NSString *)n { _name = n; } - (NSString *)name { return _name; } @end |
@implementation BNRThing @end |
These two classes in Table 3.1 are exactly the same: each has one instance variable for the name of the instance and a setter and getter for the name. On the left, you type out these declarations and instance variables yourself. On the right, you simply declare a property.
You are going to replace your instance variables and accessors in BNRItem with properties.
In BNRItem.h, delete the instance variable area and the accessor method declarations. Then, add the property declarations that replace them.
@interface BNRItem : NSObject{NSString *_itemName;NSString *_serialNumber;int _valueInDollars;NSDate *_dateCreated;BNRItem *_containedItem;__weak BNRItem *_container;}@property BNRItem *containedItem; @property BNRItem *container; @property NSString *itemName; @property NSString *serialNumber; @property int valueInDollars; @property NSDate *dateCreated; + (instancetype)randomItem; - (instancetype)initWithItemName:(NSString *)name valueInDollars:(int)value serialNumber:(NSString *)sNumber; - (instancetype)initWithItemName:(NSString *)name;- (void)setItemName:(NSString *)str;- (NSString *)itemName;- (void)setSerialNumber:(NSString *)str;- (NSString *)serialNumber;- (void)setValueInDollars:(int)v;- (int)valueInDollars;- (NSDate *)dateCreated;- (void)setContainedItem:(BNRItem *)item;- (BNRItem *)containedItem;- (void)setContainer:(BNRItem *)item;- (BNRItem *)container;@end
Now, BNRItem.h is much easier to read:
@interface BNRItem : NSObject + (instancetype)randomItem; - (instancetype)initWithItemName:(NSString *)name valueInDollars:(int)value serialNumber:(NSString *)sNumber; - (instancetype)initWithItemName:(NSString *)name; @property BNRItem *containedItem; @property BNRItem *container; @property NSString *itemName; @property NSString *serialNumber; @property int valueInDollars; @property NSDate *dateCreated; @end
Notice that the names of the properties are the names of the instance variables minus the underscore. The instance variable generated by a property, however, does have an underscore in its name.
Let's look at an example. When you declared the property named itemName, you got an instance variable named _itemName, a getter method named itemName, and a setter method named setItemName:.(Note that these declarations will not appear in your file; they are declared by the compiler behind the scenes.)Thus, the rest of the code in your application can work as before.
Declaring these properties also takes care of the implementations of the accessors. In BNRItem.m, delete the accessor implementations.
- (void)setItemName:(NSString *)str { _itemName = str; } - (NSString *)itemName { return _itemName; } - (void)setSerialNumber:(NSString *)str { _serialNumber = str; } - (NSString *)serialNumber { return _serialNumber; } - (void)setValueInDollars:(int)p { _valueInDollars = p; } - (int)valueInDollars { return _valueInDollars; } - (NSDate *)dateCreated { return _dateCreated; } - (void)setContainedItem:(BNRItem *)item { _containedItem = item; // When given an item to contain, the contained // item will be given a pointer to its container item.container = self; } - (BNRItem *)containedItem { return _containedItem; } - (void)setContainer:(BNRItem *)item { _container = item; } - (BNRItem *)container { return _container; }
You may be wondering about the implementation of setContainedItem: that you just deleted. This setter did more than just set the _containedItem instance variable. It also set the _container instance variable of the passed-in item. To replicate this functionality, you will shortly write a custom setter for the containedItem property. But first, let's discuss some property basics.
Property attributes
A property has a number of attributes that allow you to modify the behavior of the accessor methods and the instance variable it creates. The attributes are declared in parentheses after the @property directive. Here is an example:
@property (nonatomic, readwrite, strong) NSString *itemName;
Each attribute has a set of possible values, one of which is the default and does not have to be explicitly declared.
Multi-threading attribute
The multi-threading attribute of a property has two values: nonatomic or atomic.(Multi-threading is outside the scope of this book, but you still need to know the values for this attribute.)Most iOS programmers typically use nonatomic. We do at Big Nerd Ranch, and so does Apple. In this book, you will use nonatomic for all properties.
Unfortunately, the default value for this attribute is atomic, so you have to specify that you want your properties to be nonatomic.
In BNRItem.h, change all of your properties to be nonatomic.
@interface BNRItem : NSObject + (instancetype)randomItem; - (instancetype)initWithItemName:(NSString *)name valueInDollars:(int)value serialNumber:(NSString *)sNumber; - (instancetype)initWithItemName:(NSString *)name; @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
Read/write attribute
The read/write attribute's value, readwrite or readonly, tells the compiler whether to implement a setter method for the property. A readwrite property implements both a setter and getter. A readonly property just implements a getter. The default option for this attribute is readwrite. This is what you want for all of BNRItem's properties except dateCreated, which should be readonly.
In BNRItem.h, declare dateCreated as a readonly property so that no setter method is generated for this instance variable.
@property (nonatomic, readonly) NSDate *dateCreated;
Memory management attribute
The memory management attribute's values are strong, weak, copy, and unsafe_unretained. This attribute describes the type of reference that the object with the instance variable has to the object that the variable is pointing to.
For properties that do not point to objects (like the int valueInDollars), there is no need for memory management, and the only option is unsafe_unretained. This is direct assignment. You may also see the value assign in some places, which was the term used before ARC.
(The “unsafe” part of unsafe_unretained is misleading when dealing with non-object properties. It comes from contrasting unsafe unretained references with weak references. Unlike a weak reference, an unsafe unretained reference is not automatically set to nil when the object that it points to is destroyed. This is unsafe because you could end up with dangling pointers. However, the issue of dangling pointers is irrelevant when dealing with non-object properties.)
As the only option, unsafe_unretained is also the default value for non-object properties, so you can leave the valueInDollarsproperty as is.
For properties that manage a pointer to an Objective-C object, all four options are possible. The default is strong. However, Objective-C programmers tend to explicitly declare this attribute.(One reason is that the default value has changed in the last few years, and that could happen again.)
In BNRItem.m, set the memory management attribute as strong for the containedItem and dateCreated properties and weak for the container property.
@property (nonatomic, strong) BNRItem *containedItem; @property (nonatomic, weak) BNRItem *container; @property (nonatomic) NSString *itemName; @property (nonatomic) NSString *serialNumber; @property (nonatomic) int valueInDollars; @property (nonatomic, readonly, strong) NSDate *dateCreated;
Setting the container property to weak prevents the strong reference cycle that you caused and fixed earlier.
What about the itemName and serialNumber properties? These point to instances of NSString. When a property points to an instance of a class that has a mutable subclass (like NSString/NSMutableString or NSArray/NSMutableArray), you should set its memory management attribute to copy.
In BNRItem.m, set the memory management attribute for itemName and serialNumber as copy.
@property (nonatomic, strong) BNRItem *containedItem; @property (nonatomic, weak) BNRItem *container; @property (nonatomic, copy) NSString *itemName; @property (nonatomic, copy) NSString *serialNumber; @property (nonatomic) int valueInDollars; @property (nonatomic, readonly, strong) NSDate *dateCreated;
Here is what the generated setter for itemName will look like:
- (void)setItemName:(NSString *)itemName { _itemName = [itemName copy]; }
Instead of setting _itemName to point to the incoming object, this setter sends the message copy to that object. The copy method returns an immutable NSString object that has the same values as the original string, and _itemName is set to point at the new string.
Why is it safer to do this for NSString?It is safer to make a copy of the object rather than risk pointing to a possibly mutable object that could have other owners who might change the object without your knowledge.
For instance, imagine if an item was initialized so that its itemName pointed to an NSMutableString.
NSMutableString *mutableString = [[NSMutableString alloc] init]; BNRItem *item = [[BNRItem alloc] initWithItemName:mutableString valueInDollars:5 serialNumber:@"4F2W7"]];
This code is valid because an instance of NSMutableString is also an instance of its superclass, NSString. The problem is that the string pointed to by mutableString can be changed without the knowledge of the item that also points to it.
In your application, you are not going to change this string unless you mean to. However, when you write classes for others to use, you cannot control how they will be used, and you have to program defensively.
In this case, the defense is to declare itemName with the copy attribute.
In terms of ownership, copy gives you a strong reference to the object pointed to. The original string is not modified in any way: it does not gain or lose an owner, and none of its data changes.
While it is wise to make a copy of an mutable object, it is wasteful to make a copy of an immutable object. An immutable object cannot be changed, so the kind of problem described above cannot occur. To prevent needless copying, immutable classes implement copy to quietly return a pointer to the original and immutable object.
Custom accessors with properties
By default, the accessors that a property implements are very simple and look like this:
- (void)setContainedItem:(BNRItem *)item { _containedItem = item; } - (BNRItem *)containedItem { return _containedItem; }
For most properties, this is exactly what you want. However, for the containedItem property, the default setter method is not sufficient. The implementation of setContainedItem: needs an extra step: it should also set the containerproperty of the item being contained.
You can replace the default setter by implementing the setter yourself in the implementation file.
In BNRItem.m, add back an implementation for setContainedItem:.
- (void)setContainedItem:(BNRItem *)containedItem { _containedItem = containedItem; self.containedItem.container = self; }
When the compiler sees that you have implemented setContainedItem:, it will not create a default setter for containedItem. It will still create the getter method, containedItem.
Note that if you implement both a custom setter and a custom getter (or just a custom getter on a read-only property), then the compiler will not create an instance variable for your property. If you need one, you must declare it yourself.
Note the moral: sometimes the default accessors do not do what you need, and you will need to implement them yourself.
Now you can build and run the application. The leaner BNRItem works in the exact same way.