Tuesday, December 2, 2008

Outlets - Property vs. Instance Variable

When we were writing our iPhone programming book, all of Apple's sample code and documentation used the IBOutlet keyword exactly how we had always used it in Cocoa for Mac OS X, placed in front of the instance variable declaration, like so
#import <UIKit/UIKit.h>

@interface MyViewController : UIViewController {
IBOutlet UILabel *myLabel;

}
@property (nonatomic, retain) UILabel *myLabel;
@end

Somewhere along the line, I'm not exactly sure when, they started putting the IBOutlet modifier in the property declaration instead, like so:
#import <UIKit/UIKit.h>

@interface MyViewController : UIViewController {
UILabel *myLabel;

}
@property (nonatomic, retain) IBOutlet UILabel *myLabel;
@end

Now, in practice, there's absolutely no difference in the way a controller class functions based on where you place the IBOutlet keyword. I've used both approaches, even mingled within the same class, and have noticed no difference. That wasn't enough for me, though; I wanted to know if there was any underlying difference between the two - any difference in how the connections were made.

You see, early on, about the time of SDK2, I recall somebody saying that they had tested it and the mutators for their IBOutlets weren't getting called when the outlet connections were made. This came as a surprise because the sample source code was using properties for all IBOutlets, even though that wasn't the way it had been done in Cocoa on the Mac, and if the mutators weren't getting used, there wouldn't be any reason to declare a property for the outlets.

This made me wonder if, perhaps, Interface Builder only used the mutator if you put the IBOutlet keyword on the property declaration line but not if you put it preceding the instance variable declaration. There seemed to be a logic to that, given Apple's change in the preferred placement of the IBOutlet keyword from the instance variable to the property. If that's the case, then it would be important to know, because you could get slight differences in behavior in some rare circumstances based on which you used.

Wondering should lead to exploration, so I decided to find out. It's easy enough to test - I created an iPhone application project with two labels on a view, and two outlets to be connected to those two labels. One of the outlets was given the IBOutlet keyword preceding the instance variable declaration, the other was given the IBOutlet declaration as part of the property. In both cases, I implemented the mutator methods manually so that they would print a statement to the log whenever they were called in addition to doing the assignment.

And the Result?

It appears that where you place the IBOutlet keyword, on all current versions of the SDK (2.0, 2.1, 2.2), is purely stylistic and has no impact on what's happening under the hood. For both outlets, the mutator was called when the outlets were connected during application startup.

So, it seems that you can use whichever keyword placement you prefer, although Apple definitely seems to be going with placing the IBOutlet in the property declaration for all their new sample code and documentation code listings, so unless you feel strongly that it should be on the instance variable, that's probably the best place to put it, and probably where we'll put it if we ever publish a second edition.

Monday, December 1, 2008

Speaking of Errata...

...there's a doozy in Chapter 8, and I don't know how long it will take for the fix to work its way into the source code on the official book site. The problem is easy enough to fix, though, so I thought I'd post it here until the fix is in the official source code.

If you've tried to compile the Chapter 8 Cells project and are compiling against SDK 2.1 or SDK 2.2, then your program is likely crashing with an NSRangeException. Here's what's going on.

Under SDK 2.0, the NSBundle method loadNibNamed:owner:options: returned an array that had, as its first object, a proxy object at index 0. I believe that this proxy object points to File's Owner, but I'm not sure of that, and if anyone knows better, please let me know. Whatever it points to, the existence of this proxy object meant that the first instance in the nib would be located at index 1, so when we grabbed the view in the tableView:cellForRowAtIndexPath: method in the book, we grabbed the object at index 1, which is the first (and only in our case) instance in the nib.

With SDK 2.1+, the behavior was changed so that no proxy object is included in the array. As a result, the index of the first instance in the nib now becomes 0 instead of 1. Since our nib only had one instances (excluding subviews), the array only had one object, meaning that index 1 was now out of range. The fix, is easy - just change
cell = (CustomCell *)[nib objectAtIndex:1];
to
cell = (CustomCell *)[nib objectAtIndex:0];

Here's the full method, using the availability macros to make sure it compiles correctly under all current versions of the SDK:

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{

static NSString *CustomCellIdentifier = @"CustomCellIdentifier ";

CustomCell *cell = (CustomCell *)[tableView dequeueReusableCellWithIdentifier: CustomCellIdentifier];
if (cell == nil)
{
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"CustomCellView"
owner:self options:nil
]
;

// The behavior here changed between SDK 2.0 and 2.1. In 2.1+, loadNibNamed:owner:options: does not
// include an entry in the array for File's Owner. In 2.0, it does. This means that if you're on
// 2.2 or 2.1, you have to grab the object at index 0, but if you're running against SDK 2.0, you
// have to grab the object at index:0.

// You might be tempted to use UIDevice to get the system version number, however that is the
// operating system version, not the version of the SDK that the application is linked against. An
// iPhone operating running iPhone OS 2.2 can, for example, run an application linked against the
// 2.0 vesion of the SDK

// The way to handle this is to use what are called "Availability Macros". The pre-compiler macro
// __IPHONE_2_1 will be defined for SDK 2.1 and later, but not on SDK 2.0
#ifdef __IPHONE_2_1
cell = (CustomCell *)[nib objectAtIndex:0];
#else
cell = (CustomCell *)[nib objectAtIndex:1];
#endif

}
NSUInteger row = [indexPath row];
NSDictionary *rowData = [self.computers objectAtIndex:row];
cell.colorLabel.text = [rowData objectForKey:@"Color"];
cell.nameLabel.text = [rowData objectForKey:@"Name"];
return cell;
}
Also, if you compile the code from the project archive against SDK 2.0, you might encounter a warning message like this:
iPhone Development
Projects/08 Cells/CellsViewController.xib:6: warning: UIScrollView's 'Bounce
Zoom' option will be ignored on iPhone OS versions prior to 2.1.
Don't worry about that - all it means is that the nib was opened in Interface Builder on a machine with SDK 2.1+ installed, and it added an attribute that is not used in SDK 2.0. That attribute will simply be ignored when compiled against SDK 2.0.

Happy Belated Thanksgiving

As you can see, things have been a little slow the past week. Although I hadn't planned on taking the entire Thanksgiving week off, I ended up doing just that. A combination of burnout and a close family member's health problems triggered almost a week with no significant programming or writing time.

I'm now back to the grindstone, but I have to get caught up on other obligations before I'll be able to do another substantive post here.

Just another note about the book: so far, the reception has been far better than we could have hoped for. it spent all of last week in the top 500 in Amazon's ranking system, and spent much of that week below #200. Thanks to everyone who bought a copy, and especially to those people who took the time to post a review. Words cannot adequately express how grateful Dave and I are to each of you.

There are already a few errata that can be found at the book's website, in the forums. Most of them are due to differences between SDK 2.0, which was current when we wrote the book, and the 2.1 and now 2.2 versions. Apress also maintains errata for us, but most of the time, the solution shows up on the forums first, along with a more in-depth discussion.

If you find a problem, or get stuck, or just have a question, the forums are a good place to ask your question, or you can always just drop me an e-mail if you're more comfortable doing that.