In Objective-C, a protocol is a group of methods that can be implemented by any class. Protocols are essentially the same as interfaces in C#, and they both have similar goals. They can be used as a pseudo-data type, which is useful for making sure that a dynamically-typed object can respond to a certain set of messages. And, because any class can "adopt" a protocol, they can be used to represent a shared API between completely unrelated classes.
The official documentation discusses both an informal and a formal method for declaring protocols, but informal protocols are really just a unique use of categories and don't provide nearly as many benefits as formal protocols. With this in mind, this chapter focuses solely on formal protocols.
Creating a Protocol
First, let's take a look at how to declare a formal protocol. Create a new file in Xcode and select the Objective-C protocol icon under Mac OS X > Cocoa:
As usual, this will prompt you for a name. Our protocol will contain methods for calculating the coordinates of an object, so let's call it CoordinateSupport:
Click Next and choose the default location for the file. This will create an empty protocol that looks almost exactly like an interface:
// CoordinateSupport.h #import <Foundation/Foundation.h> @protocol CoordinateSupport <NSObject> @end
Of course, instead of the @interface
directive, it uses @protocol
, followed by the protocol name. The <NSObject>
syntax lets us incorporate another protocol into CoordinateSupport
. In this case, we're saying that CoordinateSupport
also includes all of the methods declared in the NSObject
protocol (not to be confused with the NSObject
class).
Next, let's add a few methods and properties to the protocol. This works the same way as declaring methods and properties in an interface:
#import <Foundation/Foundation.h> @protocol CoordinateSupport <NSObject> @property double x; @property double y; @property double z; - (NSArray *)arrayFromPosition; - (double)magnitude; @end
Adopting a Protocol
Any class that adopts this protocol is guaranteed to synthesize the x
, y
, and z
properties and implement the arrayFromPosition
and magnitude
methods. While this doesn't say how they will be implemented, it does give you the opportunity to define a shared API for an arbitrary set of classes.
For example, if we want both Ship
and Person
to be able to respond to these properties and methods, we can tell them to adopt the protocol by placing it in angled brackets after the superclass declaration. Also note that, just like using another class, you need to import the protocol file before using it:
#import <Foundation/Foundation.h> #import "CoordinateSupport.h" @interface Person : NSObject <CoordinateSupport> @property (copy) NSString *name; @property (strong) NSMutableSet *friends; - (void)sayHello; - (void)sayGoodbye; @end
Now, in addition to the properties and methods defined in this interface, the Person
class is guaranteed to respond to the API defined by CoordinateSupport
. Xcode will warn you that the Person
implementation is incomplete until you synthesize x
, y
, and z
, and implement arrayFromPosition
and magnitude
:
Likewise, a category can adopt a protocol by adding it after the category. For example, to tell the Person
class to adopt the CoordinateSupport
protocol in the Relations
category, you would use the following line:
@interface Person(Relations) <CoordinateSupport>
And, if your class needs to adopt more than one protocol, you can separate them with commas:
@interface Person : NSObject <CoordinateSupport, SomeOtherProtocol>
Advantages of Protocols
Without protocols, we would have two options to ensure both Ship
and Person
implemented this shared API:
- Re-declare the exact same properties and methods in both interfaces.
- Define the API in an abstract superclass and define
Ship
andPerson
as subclasses.
Neither of these options are particularly appealing: the first is redundant and prone to human error, and the second is severely limiting, especially if they already inherit from different parent classes. It should be clear that protocols are much more flexible and reusable, as they shield the API from being dependent on any particular class.
The fact that any class can easily adopt a protocol makes it possible to define horizontal relationships on top of an existing class hierarchy:
Due to the flexible nature of protocols, the various iOS frameworks make good use of them. For example, user interface controls are often configured using the delegation design pattern, wherein a delegate object is responsible for reacting to user actions. Instead of encapsulating a delegate's responsibilities in an abstract class and forcing delegates to subclass it, iOS defines the necessary API for the delegate in a protocol. This way, it's incredibly easy for any object to act as the delegate object. We'll explore this in much more detail in the second half of this series, iOS Succinctly.
Protocols as Pseudo-Types
Protocols can be used as psuedo-data types. Instead of making sure a variable is an instance of a class, using a protocol as a type checking tool ensures that the variable always conforms to an arbitrary API. For example, the following person
variable is guaranteed to implement the CoordinateSupport API.
Person <CoordinateSupport> *person = [[Person alloc] init];
Still, enforcing protocol adoption is often more useful when used with the id
data type. This lets you assume certain methods and properties while completely disregarding the object's class.
And of course, the same syntax can be used with a method parameter. The following snippet adds a new getDistanceFromObject:
method to the API whose parameter is required to conform to CoordinateSupport
protocol:
// CoordinateSupport.h #import <Foundation/Foundation.h> @protocol CoordinateSupport <NSObject> @property double x; @property double y; @property double z; - (NSArray *)arrayFromPosition; - (double)magnitude; - (double)getDistanceFromObject:(id <CoordinateSupport>)theObject; @end
Note that it's entirely possible to use a protocol in the same file as it is defined.
Dynamic Conformance Checking
In addition to the static type checking discussed in the last section, you can also use the conformsToProtocol:
method defined by the NSObject
protocol to dynamically check whether an object conforms to a protocol or not. This is useful for preventing errors when working with dynamic objects (objects typed as id
).
The following example assumes the Person
class adopts the CoordinateSupport
protocol, while the Ship
class does not. It uses a dynamically typed object called mysteryObject
to store an instance of Person
,and then uses conformsToProtocol:
to check if it has coordinate support. If it does, it's safe to use the x
, y
, and z
properties, as well as the other methods declared in the CoordinateSupport
protocol:
// main.m #import <Foundation/Foundation.h> #import "Person.h" #import "Ship.h" int main(int argc, const char * argv[]) { @autoreleasepool { id mysteryObject = [[Person alloc] init]; [mysteryObject setX:10.0]; [mysteryObject setY:0.0]; [mysteryObject setZ:7.5]; // Uncomment next line to see the "else" portion of conditional. //mysteryObject = [[Ship alloc] init]; if ([mysteryObject conformsToProtocol:@protocol(CoordinateSupport)]) { NSLog(@"Ok to assume coordinate support."); NSLog(@"The object is located at (%0.2f, %0.2f, %0.2f)", [mysteryObject x], [mysteryObject y], [mysteryObject z]); } else { NSLog(@"Error: Not safe to assume coordinate support."); NSLog(@"I have no idea where that object is..."); } } return 0; }
If you uncomment the line that reassigns the mysteryObject
to a Ship
instance, the conformsToProtocol:
method will return NO
, and you won't be able to safely use the API defined by CoordinateSupport
. If you're not sure what kind of object a variable will hold, this kind of dynamic protocol checking is important to prevent your program from crashing when you try to call a method that doesn't exist.
Also notice the new @protocol()
directive. This works much like @selector()
, except instead of a method name, it takes a protocol name. It returns a Protocol
object, which can be passed to conformsToProtocol:
, among other built-in methods. The protocol header file does not need to be imported for @protocol()
to work.
Forward-Declaring Protocols
If you end up working with a lot of protocols, you'll eventually run into a situation where two protocols rely on one another. This circular relationship poses a problem for the compiler, since it cannot successfully import either of them without the other. For example, let's say we were trying to abstract out some GPS functionality into a GPSSupport
protocol, but want to be able to convert between the "normal" coordinates of our existing CoordinateSupport
and the coordinates used by GPSSupport
. The GPSSupport
protocol is pretty simple:
#import <Foundation/Foundation.h> #import "CoordinateSupport.h" @protocol GPSSupport <NSObject> - (void)copyCoordinatesFromObject:(id <CoordinateSupport>)theObject; @end
This doesn't pose any problems, that is, until we need to reference the GPSSupport
protocol from CoordinateSupport.h
:
#import <Foundation/Foundation.h> #import "GPSSupport.h" @protocol CoordinateSupport <NSObject> @property double x; @property double y; @property double z; - (NSArray *)arrayFromPosition; - (double)magnitude; - (double)getDistanceFromObject:(id <CoordinateSupport>)theObject; - (void)copyGPSCoordinatesFromObject:(id <GPSSupport>)theObject; @end
Now, the CoordinateSupport.h
file requires the GPSSupport.h
file to compile correctly, and vice versa. It's a chicken-or-the-egg kind of problem, and the compiler will not like it very much:
Resolving the recursive relationship is simple. All you need to do is forward-declare one of the protocols instead of trying to import it directly:
#import <Foundation/Foundation.h> @protocol CoordinateSupport; @protocol GPSSupport <NSObject> - (void)copyCoordinatesFromObject:(id <CoordinateSupport>)theObject; @end
All @protocol CoordinateSupport;
says is that CoordinateSupport
is indeed a protocol and the compiler can assume it exists without importing it. Note the semicolon at the end of the statement. This could be done in either of the two protocols; the point is to remove the circular reference. The compiler doesn't care how you do it.
Summary
Protocols are an incredibly powerful feature of Objective-C. They let you capture relationships between arbitrary classes when it's not feasible to connect them with a common parent class. We'll utilize several built-in protocols in iOS Succinctly, as many of the core functions of an iPhone or iPad app are defined as protocols.
The next chapter introduces exceptions and errors, two very important tools for managing the problems that inevitably arise while writing Objective-C programs.
This lesson represents a chapter from Objective-C Succinctly, a free eBook from the team at Syncfusion.
Comments