Categories are an Objective-C language feature that let you add new methods to an existing class, much like C# extensions. However, do not confuse C# extensions with Objective-C extensions. Objective-C's extensions are a special case of categories that let you define methods that must be declared in the main implementation block.
These are powerful features that have many potential uses. First, categories make it possible to split up a class' interface and implementation into several files, which provides much-needed modularity for larger projects. Second, categories let you fix bugs in an existing class (e.g., NSString
) without the need to subclass it. Third, they provide an effective alternative to the protected and private methods found in C# and other Simula-like languages.
Categories
A category is a group of related methods for a class, and all of the methods defined in a category are available through the class as if they were defined in the main interface file. As an example, take the Person
class that we've been working with throughout this book. If this were a large project, Person
may have dozens of methods ranging from basic behaviors to interactions with other people to identity checking. The API might call for all of these methods to be available through a single class, but it's much easier for developers to maintain if each group is stored in a separate file. In addition, categories eliminate the need to recompile the entire class every time you change a single method, which can be a time-saver for very large projects.
Let's take a look at how categories can be used to achieve this. We start with a normal class interface and a corresponding implementation:
// Person.h @interface Person : NSObject @interface Person : NSObject @property (readonly) NSMutableArray* friends; @property (copy) NSString* name; - (void)sayHello; - (void)sayGoodbye; @end // Person.m #import "Person.h" @implementation Person @synthesize name = _name; @synthesize friends = _friends; -(id)init{ self = [super init]; if(self){ _friends = [[NSMutableArray alloc] init]; } return self; } - (void)sayHello { NSLog(@"Hello, says %@.", _name); } - (void)sayGoodbye { NSLog(@"Goodbye, says %@.", _name); } @end
Nothing new here-just a Person
class with two properties (the friends
property will be used by our category) and two methods. Next, we'll use a category to store some methods for interacting with other Person
instances. Create a new file, but instead of a class, use the Objective-C Category template. Use Relations for the category name and Person for the Category on field:
As expected, this will create two files: a header to hold the interface and an implementation. However, these will both look slightly different than what we've been working with. First, let's take a look at the interface:
// Person+Relations.h #import <Foundation/Foundation.h> #import "Person.h" @interface Person (Relations) - (void)addFriend:(Person *)aFriend; - (void)removeFriend:(Person *)aFriend; - (void)sayHelloToFriends; @end
Instead of the normal @interface
declaration, we include the category name in parentheses after the class name we're extending. A category name can be anything, as long as it doesn't conflict with other categories for the same class. A category's file name should be the class name followed by a plus sign, followed by the name of the category (e.g., Person+Relations.h
).
So, this defines our category's interface. Any methods we add in here will be added to the original Person
class at run time. It will appear as though the addFriend:
, removeFriend:
, and sayHelloToFriends
methods are all defined in Person.h
, but we can keep our functionality encapsulated and maintainable. Also note that you must import the header for the original class, Person.h
. The category implementation follows a similar pattern:
// Person+Relations.m #import "Person+Relations.h" @implementation Person (Relations) - (void)addFriend:(Person *)aFriend { [[self friends] addObject:aFriend]; } - (void)removeFriend:(Person *)aFriend { [[self friends] removeObject:aFriend]; } - (void)sayHelloToFriends { for (Person *friend in [self friends]) { NSLog(@"Hello there, %@!", [friend name]); } } @end
This implements all of the methods in Person+Relations.h
. Just like the category's interface, the category name appears in parentheses after the class name. The category name in the implementation should match the one in the interface.
Also, note that there is no way to define additional properties or instance variables in a category. Categories have to refer back to data stored in the main class (friends
in this instance).
It's also possible to override the implementation contained in Person.m
by simply redefining the method in Person+Relations.m
. This can be used to monkey patch an existing class; however, it's not recommended if you have an alternative solution to the problem, since there would be no way to override the implementation defined by the category. That is to say, unlike the class hierarchy, categories are a flat organizational structure-if you implement the same method in two separate categories, it's impossible for the runtime to figure out which one to use.
The only change you have to make to use a category is to import the category's header file. As you can see in the following example, the Person
class has access to the methods defined in Person.h
along with those defined in the category Person+Relations.h
:
// main.m #import <Foundation/Foundation.h> #import "Person.h" #import "Person+Relations.h" int main(int argc, const char * argv[]) { @autoreleasepool { Person *joe = [[Person alloc] init]; joe.name = @"Joe"; Person *bill = [[Person alloc] init]; bill.name = @"Bill"; Person *mary = [[Person alloc] init]; mary.name = @"Mary"; [joe sayHello]; [joe addFriend:bill]; [joe addFriend:mary]; [joe sayHelloToFriends]; } return 0; }
And that's all there is to creating categories in Objective-C.
Protected Methods
To reiterate, all Objective-C methods are public-there is no language construct to mark them as either private or protected. Instead of using "true" protected methods, Objective-C programs can combine categories with the interface/implementation paradigm to achieve the same result.
The idea is simple: declare "protected" methods as a category in a separate header file. This gives subclasses the ability to "opt-in" to the protected methods while unrelated classes use the "public" header file as usual. For example, take a standard Ship
interface:
// Ship.h #import <Foundation/Foundation.h> @interface Ship : NSObject - (void)shoot; @end
As we've seen many times, this defines a public method called shoot
. To declare a protected method, we need to create a Ship
category in a dedicated header file:
// Ship_Protected.h #import <Foundation/Foundation.h> @interface Ship(Protected) - (void)prepareToShoot; @end
Any classes that need access to the protected methods (namely, the superclass and any subclasses) can simply import Ship_Protected.h
. For example, the Ship
implementation should define a default behavior for the protected method:
// Ship.m #import "Ship.h" #import "Ship_Protected.h" @implementation Ship { BOOL _gunIsReady; } - (void)shoot { if (!_gunIsReady) { [self prepareToShoot]; _gunIsReady = YES; } NSLog(@"Firing!"); } - (void)prepareToShoot { // Execute some private functionality. NSLog(@"Preparing the main weapon..."); } @end
Note that if we hadn't imported Ship_Protected.h
, this prepareToShoot
implementation would be a private method, as discussed in the Methods chapter. Without a protected category, there would be no way for subclasses to access this method. Let's subclass the Ship
to see how this works. We'll call it ResearchShip
:
// ResearchShip.h #import "Ship.h" @interface ResearchShip : Ship - (void)extendTelescope; @end
This is a normal subclass interface-it should not import the protected header, as this would make the protected methods available to anyone that imports ResearchShip.h
, which is precisely what we're trying to avoid. Finally, the implementation for the subclass imports the protected methods and (optionally) overrides them:
// ResearchShip.m #import "ResearchShip.h" #import "Ship_Protected.h" @implementation ResearchShip - (void)extendTelescope { NSLog(@"Extending the telescope"); } // Override protected method - (void)prepareToShoot { NSLog(@"Oh shoot! We need to find some weapons!"); } @end
To enforce the protected status of the methods in Ship_Protected.h
, other classes aren't allowed to import it. They'll just import the normal "public" interfaces of the superclass and subclass:
// main.m #import <Foundation/Foundation.h> #import "Ship.h" #import "ResearchShip.h" int main(int argc, const char * argv[]) { @autoreleasepool { Ship *genericShip = [[Ship alloc] init]; [genericShip shoot]; Ship *discoveryOne = [[ResearchShip alloc] init]; [discoveryOne shoot]; } return 0; }
Since neither main.m
, Ship.h
, nor ResearchShip.h
import the protected methods, this code won't have access to them. Try adding a [discoveryOne prepareToShoot]
method-it will throw a compiler error, since the prepareToShoot
declaration is nowhere to be found.
To summarize, protected methods can be emulated by placing them in a dedicated header file and importing that header file into the implementation files that require access to the protected methods. No other files should import the protected header.
While the workflow presented here is a completely valid organizational tool, keep in mind that Objective-C was never meant to support protected methods. Think of this as an alternative way to structure an Objective-C method, rather than a direct replacement for C#/Simula-style protected methods. It's often better to look for another way to structure your classes rather than forcing your Objective-C code to act like a C# program.
Caveats
One of the biggest issues with categories is that you can't reliably override methods defined in categories for the same class. For example, if you defined an addFriend:
class in Person(Relations)
and later decided to change the addFriend:
implementation via a Person(Security)
category, there is no way for the runtime to know which method it should use since categories are, by definition, a flat organizational structure. For these kinds of cases, you need to revert to the traditional subclassing paradigm.
Also, it's important to note that a category can't add instance variables. This means you can't declare new properties in a category, as they could only be synthesized in the main implementation. Additionally, while a category technically does have access to its classes' instance variables, it's better practice to access them through their public interface to shield the category from potential changes in the main implementation file.
Extensions
Extensions (also called class extensions) are a special type of category that requires their methods to be defined in the main implementation block for the associated class, as opposed to an implementation defined in a category. This can be used to override publicly declared property attributes. For example, it is sometimes convenient to change a read-only property to a read-write property within a class' implementation. Consider the normal interface for a Ship
class:
Included code sample: Extensions
// Ship.h #import <Foundation/Foundation.h> #import "Person.h" @interface Ship : NSObject @property (strong, readonly) Person *captain; - (id)initWithCaptain:(Person *)captain; @end
It's possible to override the @property
definition inside of a class extension. This gives you the opportunity to re-declare the property as readwrite
in the implementation file. Syntactically, an extension looks like an empty category declaration:
// Ship.m #import "Ship.h" // The class extension. @interface Ship() @property (strong, readwrite) Person *captain; @end // The standard implementation. @implementation Ship @synthesize captain = _captain; - (id)initWithCaptain:(Person *)captain { self = [super init]; if (self) { // This WILL work because of the extension. [self setCaptain:captain]; } return self; } @end
Note the ()
appended to the class name after the @interface
directive. This is what marks it as an extension rather than a normal interface or a category. Any properties or methods that appear in the extension must be declared in the main implementation block for the class. In this case, we aren't adding any new fields-we're overriding an existing one. But unlike categories, extensions can add extra instance variables to a class, which is why we're able to declare properties in a class extension but not a category.
Because we re-declared the captain
property with a readwrite
attribute, the initWithCaptain:
method can use the setCaptain:
accessor on itself. If you were to delete the extension, the property would return to its read-only status and the compiler would complain. Clients using the Ship
class aren't supposed to import the implementation file, so the captain
property will remain read-only.
#import <Foundation/Foundation.h> #import "Person.h" #import "Ship.h" int main(int argc, const char * argv[]) { @autoreleasepool { Person *heywood = [[Person alloc] init]; heywood.name = @"Heywood"; Ship *discoveryOne = [[Ship alloc] initWithCaptain:heywood]; NSLog(@"%@", [discoveryOne captain].name); Person *dave = [[Person alloc] init]; dave.name = @"Dave"; // This will NOT work because the property is still read-only. [discoveryOne setCaptain:dave]; } return 0; }
Private Methods
Another common use case for extensions is for declaring private methods. In the previous chapter, we saw how private methods can be declared by simply adding them anywhere in the implementation file. But, prior to Xcode 4.3, this was not the case. The canonical way to create a private method was to forward-declare it using a class extension. Let's take a look at this by slightly altering the Ship
header from the previous example:
// Ship.h #import <Foundation/Foundation.h> @interface Ship : NSObject - (void)shoot; @end
Next, we're going to recreate the example we used when we discussed private methods in the Methods chapter. Instead of simply adding the private prepareToShoot
method to the implementation, we need to forward-declare it in a class extension.
// Ship.m #import "Ship.h" // The class extension. @interface Ship() - (void)prepareToShoot; @end // The rest of the implementation. @implementation Ship { BOOL _gunIsReady; } - (void)shoot { if (!_gunIsReady) { [self prepareToShoot]; _gunIsReady = YES; } NSLog(@"Firing!"); } - (void)prepareToShoot { // Execute some private functionality. NSLog(@"Preparing the main weapon..."); } @end
The compiler ensures the extension methods are implemented in the main implementation block, which is why it functions as a forward-declaration. Yet because the extension is encapsulated in the implementation file, other objects shouldn't ever know about it, giving us another way to emulate private methods. While newer compilers save you this trouble, it's still important to understand how class extensions work, as it has been a common way to leverage private methods in Objective-C programs until very recently.
Summary
This chapter covered two of the more unique concepts in the Objective-C programming language: categories and extensions. Categories are a way to extend the API of existing classes, and extensions are a way to add required methods to the API outside of the main interface file. Both of these were initially designed to ease the burden of maintaining large code bases.
The next chapter continues our journey through Objective-C's organizational structures. We'll learn how to define a protocol, which is an interface that can be implemented by a variety of classes.
This lesson represents a chapter from Objective-C Succinctly, a free eBook from the team at Syncfusion.
Comments