Sunday, December 13, 2009

Using NSSet instead of plain old C-Enums and bitwise operations

In my first blog post I would like to convince you that using C-Enums and bit operations when writing Objective-C code is not such a good idea anymore. In order to convince you I will first show you how we deal with enumerations today. In the second act I will explain why the current handling of enumerations is bad. And at last I will show you a alternative that you can use to improve you enum and bit operation code.

Now, please have a look at NSView's autoresizingMask property. The autoresizingMask lets a developer specify how the view will change it's frame when it is being resized. When setting the auto resizing behavior you have create a mask using bitwise OR operations. Apple explains that in the documentation quite well:

- (void)setAutoresizingMask:(NSUInteger)mask

Parameters
mask
An integer bit mask. mask can be specified by combining using the C bitwise OR operator any of the options described in "Resizing masks".

The possible "values" that can be used to composite an auto resizing mask are also well documented:

enum {
   NSViewNotSizable     = 0,
   NSViewMinXMargin     = 1,
   NSViewWidthSizable   = 2,
   NSViewMaxXMargin     = 4,
   NSViewMinYMargin     = 8,
   NSViewHeightSizable  = 16,
   NSViewMaxYMargin     = 32
};

So, how does setting the auto resizing mask typically look like? Let's recap:

[aView setAutoresizingMask:(NSViewWidthSizable |
                            NSViewHeightSizable)];

One questions immediately pops up: Why does it have to be so complicated? There are good reasons.

  • Bit masks are usually very "small" and thus save memory.
  • Even though a resizing mask is nothing more than a single number it can describe nearly an infinite number of possible combinations. There is no need for setters/getters like -setHeightSizable:(BOOL)flag, ...

What is wrong with that? At the time Objective-C and Cocoa were first invented CPU speed and available memory were very limited resources. Thus using enumerations and bitwise operations to describe complex properties was a brilliant idea. But today, these concepts seem a little bit outdated, at least on the desktop platform. So let's improve that.

Let's try to describe to autoresizingMask-property in a modern way. The first step is to find an equivalent to a "mask". The most obvious equivalent is a simple mathematical set that is able to contain the possible mask values. Fortunately Objective-C (Foundation) does already know what a set is. A set is just an instance of NSSet. Brilliant. But wait! It is not possible to add plain old C objects into a NSSet. Well, we don't have to. Instead of assigning the mask values like NSViewHeightSizable unsigned integer values we assign each of them a constant NSString.

NSString *kViewNotSizable = @"kViewNotSizable";
NSString *kViewMinXMargin = @"kViewMinXMargin";
NSString *kViewWidthSizable = @"kViewWidthSizable";
NSString *kViewMaxXMargin = @"kViewMaxXMargin";
NSString *kViewMinYMargin = @"kViewMinYMargin";
NSString *kViewHeightSizable = @"kViewHeightSizable";
NSString *kViewMaxYMargin = @"kViewMaxYMargin";

We would also have to rewrite the accessors for the auto resizing mask to use NSSet.

- (void)setAutoresizingMask:(NSSet *)mask;
- (NSSet *)autoresizingMask;

Thats basically it. Setting the auto resizing mask by using this new pattern would look like this:

[aView setAutoresizingMask:[NSSet setWithObjects:
                                    kViewWidthSizable,
                                    kViewHeightSizable, nil];

No more bitwise operations. But there is more: By using NSSet we can now use the "operations" that are already built in NSSet. Let me give you just two examples:

Checking if the view should resize it's height:

if([[aView autoresizingMask] containsObject:kViewHeightSizable])

Checking if the auto resizing mask is "valid":

 NSSet *maxMask = [NSSet setWithObjects:
                   kViewNotSizable,
                   kViewMinXMargin,
                   kViewWidthSizable,
                   kViewMaxXMargin,
                   kViewMinYMargin,
                   kViewHeightSizable,
                   kViewMaxYMargin];

 if([[aView autoresizingMask] isSubsetOfSet:maxMask])

I hope you get the idea. Sure, you have to write a few extra lines of code. But it becomes much more readable. In addition to that key value coding and key value observing are possible and safe without doing additional work. If you want that the size of a view should stay the same just assign is an empty set. You don't have to know or lookup the value that represents that.

I suggest that you try this pattern for yourself where you can. I am pretty sure that you will fall in love with it. You don't have to worry about performance of memory related problems because I am sure that it doesn't have a significant impact. When developing for the iPhone/iPod platform you may want to consider not using this pattern. I can imagine that it could have a negative effect there. But I haven' tried it. That is up to you. :)

Let me know that you think about this "pattern". Will you try it? Are you already using it? Post a comment or send me an email.