Understanding Categories
What is a Category?
Objective-C has a really cool feature called Categories. In short, a Category gives developers an opportunity to add additional methods to existing classes. Does NSString not have a method you wish it did? You can go ahead and write the method and add it to NSString, without needing the original source code. Apple explains this in their documentation like such:
Any methods that you declare in a category will be available to all instances of the original class, as well as any subclasses of the original class. At runtime, there’s no difference between a method added by a category and one that is implemented by the original class.
One thing you want to watch out for however is properties. You can only add methods to an existing class via Categories; never properties. My tinkering around with them the other day showed that ivars are not allowed either. I'm not sure why this restriction is in place, but that's the way Apple designed them. You can add a property like @property (nonatomic, strong) NSString *foo;
and access it without a problem; the compiler might complain a bit, but that can be silenced by adding a @dynamic
above your property. All that does however is silence the warnings that your property is not being properly synthesized. Apple's documentation tells us that the class that the category is adding methods to, will not synthesize any properties in the categories. So the issue that comes from this is the inability to track the values associated with our properties, once the code leaves our setter/getter methods. The backing ivar's are never synthesized.
So long story short, don't use properties in your categories. If you need to use a property, extend the class with a class extension or sub-class it.
Putting it into practice
We now know what a category is, but what would you ever need to use one for? Once you get used to how categories work, it's surprisingly easy to find a use for them on a regular basis. For instance, tonight I ran into an issue with my code were I had two NSDate objects that had the same hour and minute, but were off by a few seconds because of the delay in creating the two dates. Since the seconds were irrelevant to what I was doing, I wanted a easy way to zero them out which would allow them to be equal to each other.
The most obvious answer is to use NSDateComponents, which is a real pain. It requires several lines of code that would need to be re-wrote over and over.
@implementation TestViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Get the current date.
NSDate *date = [NSDate date];
NSLog(@"Date is %@", date);
// Only pull the year, month, day, hour and minute. Ignore seconds.
NSUInteger unitFlags = (NSYearCalendarUnit
| NSMonthCalendarUnit
| NSDayCalendarUnit
| NSHourCalendarUnit
| NSMinuteCalendarUnit);
// Instance our calendar
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
// Filter out what components we want from the date
NSDateComponents *dateComponents = [calendar components:unitFlags fromDate:date];
// Adjust the date using our date components
NSDate *correctedDate = [calendar dateFromComponents:dateComponents];
NSLog(@"Corrected date is %@", correctedDate);
}
@end
This provides me with the following output, which verifies that the code is indeed, zeroing out the seconds.
[6755:60b] Date is 2013-11-22 08:05:25 +0000
[6755:60b] Corrected date is 2013-11-22 08:05:00 +0000
Now, instead of re-using this code all over the place, I can make a helper method in what ever object is using this. Although, I might want to use this in other classes, or even in another project! So why don't I move the code over to a category instead? Let's do that, using the same code above.
You add a new category file to your project via File->New->File and selecting a Objective-C Category
under the Cocoa Touch
or Cocoa
platforms; depending on if you are developing on iOS or OS X.
Press next and give it a category name of MyDateCategory. For the Category On field, type in NSDate
and press Next and save the file to your project folder.
Alright, now we have two new files. A NSDate+MyDateCategory.h
header file and a NSDate+MyDateCategory.m
implementation file. Let's start off with the header file which should look like this:
#import <Foundation/Foundation.h>
@interface NSDate (MyDateCategory)
@end
You can see that the actual header is an interface for NSDate. Our custom category is shown in parentheses, which lets the compiler know we are adding stuff to the existing NSDate class. We want to add our code to zero out the seconds right? So we need a new instance method which we will call dateWithZeroSeconds
.
#import <Foundation/Foundation.h>
@interface NSDate (MyDateCategory)
- (NSDate *)dateWithZeroSeconds;
@end
Our new instance method returns a new NSDate, which will let us just invoke this method and assign our date with the adjusted date. The implementation then would look like this in the .m file.
#import "NSDate+MyDateCategory.h"
@implementation NSDate (MyDateCategory)
- (NSDate *)dateWithZeroSeconds {
// Get the current date.
NSLog(@"Date is %@", self);
// Only pull the year, month, day, hour and minute. Ignore seconds.
NSUInteger unitFlags = (NSYearCalendarUnit
| NSMonthCalendarUnit
| NSDayCalendarUnit
| NSHourCalendarUnit
| NSMinuteCalendarUnit);
// Instance our calendar
NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
// Filter out what components we want from the date
NSDateComponents *dateComponents = [calendar components:unitFlags fromDate:self];
// Adjust the date using our date components
NSDate *correctedDate = [calendar dateFromComponents:dateComponents];
NSLog(@"Corrected date is %@", correctedDate);
return correctedDate;
}
@end
It's the same code that we wrote above right? With the exception that now we can use it in any project that has our NSDate+MyCustomDate.h and .m files included. Now one of the important differences here however is to note my use of the keyword self
. Since we are adding this method to NSDate, it makes since that I need to get the date components from myself, because I am the date at this point. Not another object, so we ask for the components from self
. You'll want to remember that when you are writing a category method, all of the class properties and methods are available to you; just be sure to access them via the self
keyword.
So returning to our ViewController, we would use this method like such:
- (void)viewDidLoad
{
[super viewDidLoad];
NSDate *date = [NSDate date];
NSDate *correctedDate = [date dateWithZeroSeconds];
}
And we can shorten this up even more by passing our dateWithZeroSeconds method the returned object from [NSDate date]
- (void)viewDidLoad
{
[super viewDidLoad];
NSDate *correctedDate = [[NSDate date] dateWithZeroSeconds];
}
Really nice right? Now anywhere in your code, you can use your custom method just like it was part of a NSDate object, without having to sub-class NSDate!
Now, I said earlier that using the NSDateComponents
object is more work than what you really need to do for something like this, so I'm going to revise our Category method with something more elegant.
Like such:
- (NSDate *)dateWithZeroSeconds
{
NSTimeInterval time = floor([self timeIntervalSinceReferenceDate] / 60.0) * 60.0;
return [NSDate dateWithTimeIntervalSinceReferenceDate:time];
}
Now isn't that prettier?
Hopefully this helps those that read it understand Categories in Objective-C.