Advertisement

Objective-C Succinctly: Protocols

by

This Cyber Monday Tuts+ courses will be reduced to just $3 (usually $15). Don't miss out.

This post is part of a series called Objective-C Succinctly.
Objective-C Succinctly: Categories and Extensions
Objective-C Succinctly: Exceptions and Errors

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:

Figure 29 Xcode icon for protocol files

Xcode icon for protocol files

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:

Figure 30 Naming the protocol

Naming the protocol

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:

Figure 31 Incomplete implementation warning for Person CoordinateSupport

Incomplete implementation warning for Person <CoordinateSupport>

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:

  1. Re-declare the exact same properties and methods in both interfaces.
  2. Define the API in an abstract superclass and define Ship and Person 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:

Figure 32 Linking unrelated classes using a protocol

Linking unrelated classes using a protocol

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:

Figure 33 Compiler error caused by circular protocol references

Compiler error caused by circular protocol references

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.
Advertisement