Published on MacDevCenter (
 See this if you're having trouble printing code examples

Programming With Cocoa Plug It In, Plug It In

by Michael Beam

If you've spent much time reading up on Cocoa and Objective-C, you've probably heard that Objective-C is a dynamic language. Ezra Epstein evangelizes this advantage of Objective-C in two excellent columns worth your time to read. In this column, we're going to talk a bit about Objective-C and how to implement a plug-in architecture for ImageApp.

When an application is compiled, it is linked to certain frameworks required by that application. Most of the time the frameworks we link to are the Foundation framework and the Application Kit. When an application is launched, every class in each linked framework is loaded into the Objective-C runtime for that application. So when we launch an application like ImageApp, all the classes, protocols, and categories for the Foundation Framework, AppKit, and ImageApp itself are loaded into the Objective-C runtime. These ideas are elaborated upon in Ezra Epstein's articles mentioned previously.

Now, it used to be that once an application was launched, and the code it needed to run was loaded into memory, that was the end of the story. Needless to say, the inability to load new code into memory and integrate it with the application already running is somewhat limiting. In Objective-C, the ability to load new classes into the runtime is a painless process. The Objective-C runtime relies on the function objc_addClass to load new class definitions into the runtime for an application. We won't detail the use of this function here, but you can read more about it and the Objective-C runtime in Apple's Inside Mac OS X: The Objective-C Programming Language (formerly Object-Oriented Programming and the Objective-C Language).

Our purpose today is to learn a bit about the practical benefits of these features of Objective-C as they are implemented in Cocoa. In Cocoa, this behavior is encapsulated in the class NSBundle, found in the Foundation Framework (with extensions that support nib loading defined in the AppKit). NSBundle is a class that represents a bundle in the file system and provides an interface to loading the code and resources contained within the bundles. This class is the basis of Cocoa's plug-in architecture, and we will spend a good part of the column seeing this class in action.

A Bit About Bundles

Download example files for this article here.

Before we can get to the nitty-gritty code, we have to learn more about bundles, a central feature of Mac OS X. A bundle is a directory structure in the filesystem that stores in one location binary executables or other compiled pieces of code, along with any associated configuration files, localization files, and resources, such as images, sounds, and nibs. Bundles are omnipresent in Mac OS X. Applications are bundles with .app extensions; frameworks are bundles with a .framework extension; system menus are bundles with a .menu extension, and preference panes and screensavers are bundles as well.

Often the Finder presents bundles to the user as a single file. This is true with applications and menu items bundles. This has the advantage of insulating the user from the nuts and bolts and resources used by an application, stuff most users don't really care about, but let people like us dork out by opening nibs we shouldn't be and modifying images and other resources in the name of customization. To learn more about bundles and how they are used in Mac OS X, you should read about them in the Mac OS X system overview: Inside Mac OS X: System Overview.

Related Reading

Learning Cocoa with Objective-C
By James Duncan Davidson, Apple Computer, Inc.

Now, what does this have to do with plug-ins? Well, plug-ins in Cocoa are nothing more than bundles. Usually these bundles have a .plugin extension, but .bundle is commonly seen as well. Using NSBundle it is then possible to locate a plug-in bundle, and register with the Objective-C runtime the compiled classes contained within the bundle. However, for this to work well, you have to set up something of a plug-in architecture for your application, which is exactly what we're going to do today.

This will be done in two steps. First we will create an interface to our plug-in system and learn how to create a plug-in bundle, and second we will add support into ImageApp to discover installed plug-ins and make them useable by the user.

In the last column before we took our AddressBook break, we learned how to make an image filter class that converts a color image into a grayscale image. Filters are a good candidate for implementing as plugins for lots of reason. First of all, they are relatively simple: put some data in, let the filter do its thing, get some data out. Interaction with the parent application is pretty minimal. Additionally, there are a wide range of image manipulation operations that can be accomplished with filters, and many people may have a need for a special filter to fill in some specific functionality not included in the original application. Here plugins allow developers and advanced users of your application to develop modules that fill in special functionality.

To this end we will be creating a plug-in architecture for ImageApp. This will involve defining some standard that describes to developers what the plug-in socket looks like. We will also have to create a plug-in manager within ImageApp that searches for plug-ins and makes them accessible in the user interface.

A Public Interface

If other developers are to successfully develop plug-ins for your application, you must publish some interface or specification to your system. With Objective-C, we have a couple of options available to accomplish this task. One way of publishing your plug-in system is to create a formal protocol. Plug-in developers would then create a plug-in by creating a class that conforms to this formal protocol.

Another option--the one that we will implement here--is to create a public class interface, which plug-in developers would then subclass. With our filter plug-in we don't need to do much other than make public the method filterImage:. If you recall from the column where we made the grayscale filter, this method takes an NSImage, applies some filtering operations on it, and returns a new NSImage. This was all done in the class IAGrayscaleFilter. Today we will make an interface to the class IAFilter which defines the method filterImage:. We will then have IAGrayscaleFilter inherit from this class. The purpose of IAFilter is to create a common point of communication between us, the developers of ImageApp, and any other developers who would like to create an image filter plug-in.

Our first step is to create a new class, IAFilter. Do this now from the File menu, choosing to create a simple Objective-C class. Name it IAFilter. Add to IAFilter.h the single method declaration for filterImage:

- (NSImage *)filterImage:(NSImage *)srcImage; 

This is the same method that we declared previously in IAGrayscaleFilter.

By default, the header for the Foundation framework is imported into our class. However, we include NSImage as part of our class declaration, so we need to let the compiler know about that. We'll do that by replacing Foundation and Foundation.h with Cocoa and Cocoa.h in the import statement.

As part of the public interface we will also declare a convenience constructor for all filters, filter. Do this now in IAFIlter.h as follows:

+ (IAFilter *)filter;

The implementation for filter is simple. Its only job is to alloc and init an instance of IAFilter, or a subclass of IAFilter depending on the receiver of the filter message in ImageApp. In keeping with the conventions of Cocoa memory management, we will autorelease this instance of IAFilter just before returning it, as filter is no more than a convenience constructor. Clients should always assume that convenience constructors return an autoreleased object. The filter method looks like this:

+ (IAFilter *)filter
    return [[[self alloc] init] autorelease]; 

Notice that the alloc message was sent to self, not IAFilter. This ensures that in situations when the client is instantiating a subclass of IAFilter, that the alloc and init methods of the subclass are invoked, rather than those of IAFilter. It may be that the subclass has no implementation of its own for alloc and init, and relies instead on the superclass's implemenation of these methods, but if they were there, they would be used.

Notice that the initial alloc message was sent to self, not IAFilter. This ensures that in situations when the client is creating a subclass of IAFilter, that the alloc and init methods of the subclass are invoked, if the subclass did indeed override one or both of those methods.

IAFilter's implementation of filterImage: will be to simply return the image passed to it, like so:

- (NSImage *)filterImage:(NSImage *)srcImage 
    return srcImage; 

Note that the filterImage: method is an instance method rather than a class method. Given the implementation of filterImage: that we explored in the previous column, we could have done the same thing we did there in a class method. The reason for this is that we never accessed any instance variables in the filter object. That is one of the key differences between class and instance methods. Class methods may not access instance variables, while instance methods may.

In this situation, when filterImage: does not access instance variables, it is a strictly processing method. That is, it takes input, does something to it, and spits the result back out to the sender. However, by making filterImage: an instance method here, we give subclassers the flexibility to declare and interact with instance variables, which would not have been possible had filterImage: been a class method. This flexibility makes it possible for developers to create plug-ins that have user interfaces built with Interface Builder.

We now have a class, IAFilter, that people can use to hook into the ImageApp plug-in system. The next thing to do is take the class IAGrayscaleFilter out of ImageApp, and put it into its own bundle. Before we get into that, we must do one last thing to IAGrayscaleFilter, and that is to make it a subclass of IAFilter, rather than NSObject. Do this now by replacing NSObject in the @interface line with IAFilter. Make sure you import IAFilter.h into the IAGrayscaleFilter header.

The Packaging

We now have a public interface available to interact with the ImageApp plug-in system. With that in place, the next step is to learn how to package IAGrayscaleFilter into a bundle that can be loaded by ImageApp at runtime. Rather than creating a new project for our plug-in, which would be what a third-party ImageApp plug-in developer would have to do, we will create a new target in our existing project.

A target in Project Builder is a set of rules that tells Project Builder how to build an application, a framework, a bundle, and more. When you create a project, the default target created for you is a target to build whatever you choose the project to be (i.e. Cocoa application, Cocoa bundle, Foundation tool, etc). We will add a target to the ImageApp project to build our filter plug-in.

From the Project menu select New Target, which will bring up a dialog asking you to select the type of target. Select Bundle from the top of the list, and name the target Grayscale Filter.

We now have two targets: one for the ImageApp application, and one for the Grayscale Filter plug-in bundle. If you click on the target tab, you will see a list of the targets that are a part of your project.

Screen shot.

Selecting a target will bring up a view with an outline that lets you set various options about included files, the build process, and other settings. It would be worth your while to take some time to explore the different settings available. We will see quite a few today.

If you look again at the Targets list, you will notice that each target has a disclosure triangle to the left of the name. What these disclosure triangles do is to reveal target dependencies. If target A needs needs to use the product of target B in its build process, then target A is said to be dependent on target B. We will make ImageApp dependent on the Grayscale Filter plug-in. In practice, what this does for us is automatically build Grayscale Filter when we build ImageApp. To create a dependency, all you have to do is drag the Grayscale Filter target name underneath the ImageApp (Itís really ImageAppTest) target name.

Screen shot.

The next thing we must do is add some files to our bundle. In Project Builder's toolbar you should see a pop-up button that contains a list of all of the targets in our project. Selecting a target from that list will make it the active target, which is the one that gets built or debugged when we initiate those actions. You can also select the active target by clicking on the radio buttons to the left of the target names in the targets list.

When switching between targets in this menu, you should notice that the little checkboxes to the left of each file in the Groups & Files list change depending on which target is selected. At this point they should all be selected when ImageApp is the active target, and they should all be disabled when Grayscale Filter is the active target. What these checkboxes indicate is what targets use what files. We need to change things so that the class files for IAGrayscaleFilter are used by the Grayscale Filter bundle target and not the ImageApp target.

To do this, select the ImageApp target, and uncheck the checkboxes for IAGrayscaleFilter.h and IAGrayscaleFilter.m. This removes the IAGrayscaleFilter class from ImageApp's build process. Now, select the Grayscale Filter target, and check the checkboxes for these two same files. Since IAGrayscaleFilter will soon be modified to inherit from IAFilter, we need to add IAFilter.h and IAFilter.m to this same target as well (It is perfectly acceptable for a file to be used by multiple targets in the same project).

Screen shot.

Another way you can change which files for a target to use is in the target settings tab in the Build Phases section. Here you will see sections for Headers, Sources, and more. If you click on one of the sections of Build Phases, you will see a list of all files that fall in that category. To add files to a phase, you can drag any of those files from the Groups & Files list. To delete, simply select the file and hit the delete key.

Now that we have a handle on how Project Builder handles multiple targets, let's fine-tune our Grayscale Filter bundle settings. We will make changes to the following areas:

Info.plist Entries

Related Reading

Building Cocoa Applications: A Step by Step Guide
By Simson Garfinkel, Michael Mahoney

Every bundle in Mac OS X, whether it be an application or a framework, contains a file called Info.plist. This file contains information about the bundle needed by the operating system and other applications that may use that bundle. The plug-in loader that we build for ImageApp will require the existence of two entries in this file: the principal class and a string that will be used as the filter plug-ins menu item title.

Like all property lists, Info.plist's entries are made up of key-value pairs. The key for the bundle's principal class already exists. We just have to give it a value. The key for the filter menu item title, however, is something that we must define ourselves.

To set the principal class of the bundle, select from the Targets tab Grayscale Filter and navigate to Info.plist Entries > Simple View > Cocoa-Specific, and you will see the Principal class field. Enter IAGrayscaleFilter here. We will see in a moment how ImageApp can ask the bundle to return the class object with this class name, which we will use to create an instance of IAGrayscaleFilter. You will also notice another field in this view, Main nib file. If you were building a plug-in with an interface built in Interface Builder, this is where you would specify the nib file containing that interface. An application can then use methods of NSBundle to load this nib. We won't be doing that here.

To define a custom key, click on Info.plist Entries > Expert View. Here you will see a table of all the keys and values that will be built into the bundle's Info.plist file. At the bottom of the list you should see the key NSPrincipalClass with the class name we just entered as the value. To add a new key, click on the New Sibling button at the top of the view. For the key name enter IAFilterMenuItemTitle, and for the value enter Grayscale. This is the string that will appear in the filter menu of ImageApp at runtime.

That's all we have to do as far as Info.plist is concerned. Now we have to change some settings related to how the bundle is built, and to build phases of ImageApp so the plug-in is copied to the ImageApp bundle.

Build Settings

By default bundles take on the extension .bundle. We want to change this to .plug-in to indicate that the bundle has a more specific purpose as that of a bit of code that extends the functionality of an app. If you click on Settings > Expert View, you will see a table similar to the one we were just dealing with. At the bottom of this list is the WRAPPER_EXTENSION setting. We want to change the value of WRAPPER_EXTENSION from bundle to plug-in. Do that now by double-clicking on bundle to select the existing text.

Don't worry if you don't see this change updated in the Groups & Files list where Grayscale Filter is listed under Products. It may still be listed as Grayscale Filter.bundle. The change won't be reflected in the list unless you edit the bundle name or build the bundle.

The next thing we have to do is specify the bundle's installation location. This is found under Settings > Simple View > Installation Settings. Select the Path radio button, and enter the path @executable_path/../Plug-ins.

One last thing we have to do that will prove most helpful is to provide the Cocoa frameworks for the Grayscale Filter bundle to link against. To do this, drag the item from the Groups & Files list Frameworks/Linked Frameworks/Cocoa.framework, and drop it on the Frameworks and Libraries build phase of Grayscale Filter.

Now, that's about all we have to do for the Grayscale Filter bundle. The next thing on the docket is to fix up ImageApp to incorporate this bundle into its build process.

Build Phases

In the Build Phases portion of the Targets tab we can specify what header and source files to include in the build, what frameworks and libraries to link to, and more. We've already met the Headers and Sources build phases previously when we moved the IAGrayscaleFilter class from ImageApp to the plug-in bundle. What we're interested in doing now is adding a Copy Files build phase to ImageApp.

By default there is no Copy Files build phase, so we have to add one. This is easily done by selecting from the Project menu New Build Phase > New Copy Files Build Phase. This will add the Copy Files build phase to your list of build phases. To add files to the copy phase, just drag them from the Groups & Files list onto the desired build phase. In our case we will drag Grayscale Filter.plug-in (or Grayscale Filter.bundle) into the Copy Files phase. If you haven't build the Grayscale Filter bundle yet, the filename will be red, indicating that the bundle has a meta-existence in the project, but not a physical existence in the filesystem.

After adding the Grayscale Filter to the Copy Files phase, you have to specify where we want the files copied to. Plug-ins go in the Plug-ins directory, which can be specified by selecting Plug-ins from the pop-up menu in the Copy Files view.

Screen shot.

We're almost to the point that we can get everything to build correctly. If you attempt to build now you will get compile errors with respect to the absence of IAGrayscaleFilter in ImageApp, since it is no longer included in the build process. The only instance of IAGrayscaleFilter is found in the class MyDocument. All we need to do is remove the method makeGrayscale from the interface and the implementation and remove any import statements importing IAGrayscaleFilter.h.

With all of these changes in place you should now be able to built ImageApp, have it automatically build the filter bundle to satisfy the target dependency, and copy Grayscale Filter.plug-in into the ImageApp bundle. You can check that everything went off without a hitch by showing the Info for ImageApp in the finder. If the plug-in copied correctly, you will see a Plug-ins section in the Info window containing Grayscale Filter.plug-in.

Screen shot.

The Plug-in Manager

What we have just described above is how to package code up in a bundle. In the remainder of this column we will discuss the creation of a class that will scan for plug-ins when ImageApp launches and add any discovered plug-ins to the filter menu in ImageApp. The class we create will be called IAPlug-inManager, and it will exist as an instance within MainMenu.nib.

In Interface Builder

Open MainMenu.nib now. From the Classes tab in the nib window, select NSObject and press return to create a subclass. Name the subclass IAPlug-inManager. To this class we want to add one outlet named filterMenu. This is the outlet we will use to access the filter menu in the menu bar. When you create this outlet, it will be helpful if we set the class of the outlet to NSMenu. Sometimes connecting an outlet to a menu can be tricky, and this will help to make sure we get it right. Next create the project files for IAPlug-inManager, and initiate it.

To connect the outlet to the filter menu, you have to make sure that the filter menu is open. You cannot make the connection to the label on the main menu bar that says filter. That is a menu item of the main menu. So click on the filter menu to open it. Now, when you drag your connection wire from IAPlug-inManager to the filter menu, you have to drop it on the filter menu label, not on any of the menu items.

Screen shot.

The last thing we need to do here is to remove the Make Grayscale item from the filter menu. Since all the items for this menu will be added programmatically by our plug-in manager, we want this to be empty. With that, save your work, and return to Project Builder.

Back in Project Builder

IAPlug-inManager is comprised of four methods:

The first one, awakeFromNib, is easy enough:

- (void)awakeFromNib { 
    [self discoverPlug-ins];  
    [self setupFilterMenu]; 

This method simply invokes the method discoverPlug-ins and setupFilterMenu.

The purpose of discoverPlug-ins is to search for any installed plug-ins. In Mac OS X, plug-ins may be installed in a number of well-known locations:

In today's column we set things up so that Grayscale Filter is copied to the first of these locations, within ImageApp's bundle. However, to require a user put all of their plug-ins in this single location causes a serious problem. When it comes time to upgrade the app, any user-installed plug-ins will be lost when the new version of the app is copied over the old. Thus, users should keep their plug-ins in one of the Library folders so they are available after upgrading the application.

The goal of discoverPlug-ins will be to enumerate the contents of each of the potential plug-in directories, and whenever we come across a bundle with the plug-in extension we will add an instance of that bundle's principal class to a dictionary with the filter menu item title as the key. Before getting to dicovers, go into IAPlug-inManager.h and add the NSMutableDictionary instance variable plug-ins to the class interface. Additionally import the headers for MyDocument and IAFilter.

Now, Here is what discoverPlug-ins looks like:

- (void)discoverPlug-ins
    Class filterClass;
			 NSString *appSupport = @"Library/Application Support/ImageApp/Plug-ins/";
			 NSString *appPath = [[NSBundle mainBundle] builtInPlug-insPath];
		  NSString *userPath = [NSHomeDirectory 
		  NSString *sysPath = [@"/" 
		  NSArray *paths = [NSArray arrayWithObjects:appPath,
			          userPath, sysPath, nil];
		  NSEnumerator *pathEnum = [paths objectEnumerator];
		  NSString *path;
		  plug-ins = [[NSMutableDictionary alloc] init];
		  while ( path = [pathEnum nextObject] ) {
		    NSEnumerator *e = [[[NSFileManager defaultManager]
			                directoryContentsAtPath:path] objectEnumerator];
		    NSString *name;

            while ( name = [e nextObject] )
		      if ( [[name pathExtension] isEqualToString:@"plugin"] ) {
		        NSBundle *plugin = [NSBundle bundleWithPath: name];
		        if ( filterClass = [plug-in principalClass] )
		          if ( [filterClass 
                  instancesRespondToSelector:@selector(filterImage:)] ) {
	             NSString *menuTitle = [[plug-in infoDictionary]
		            [plug-ins setObject:[filterClass filter] forKey:menuTitle];

And there you have it. Letís take our time to pick this apart. It's actually simpler than all the enumerators. The variable filterClass holds the class object returned that we obtain from the bundle.

In the next line we define the path to the plug-in directory found in the Library paths. In the next three lines we define the three paths where we will look for plug-ins. The first path is the Plug-ins directory found in the ImageApp bundle. This path is obtained using the NSBundle method builtInPlug-insPath. This method is invoked in the object returned by the mainBundle class method of NSBundle. The method mainBundle returns the NSBundle object representing the application's main bundle, which in our case is the ImageApp application bundle.

The next path is the path to the Plug-ins directory in the user's Library. This path is created by appending the common appSupport path to the user's home directory, which is obtained using the Foundation function NSHomeDirectory.

The final path is the location of the system-wide Plug-ins directory found at /Library/Application Support/ImageApp/Plug-ins/. This path is created by appending the common appSupport path to the root directory, /.

These three paths are collected together into an array, from which we obtain an enumerator assigned to the variable pathEnum. Before getting to the nested control structures, we create an NSString variable path that will be used in the enumeration of paths, and we initialize the instance variable plug-ins to an empty mutable dictionary.

Now, the heart of it. What we're doing in this unattractive nest of control structures is first enumerating the three Plug-in paths, we've defined, and for each of those three paths we obtain the contents of that directory. We then enumerate through those contents and check the extension for each object in that directory. If the path extension is plug-in, then we create an instance of NSBundle from that path.

Next we try to obtain the principal class of the bundle and assign it to filterClass. This is done by sending a principalClass method to the bundle instance. If there is no principal class defined, then the principalClass method will return Nil (The uppercase "N" is not a typo. Nil is the equivalent of nil for class objects) and we continue with the enumeration.

If there is a principal class, then we check to see if an instance of this class responds to the filterImage: message, which is essential if this is all to function correctly. To check if an instance of a class implements a specific method, we can use the NSObject method instancesRespondToSelector:. This method takes a selector as an argument, and in the code above, we pass @selector(filterImage:) since that is the method in question.

Finally, if there is a principal class in the bundle and it responds to filterImage:, we add an instance of the principal class to the dictionary plug-ins. The key for the filter instance in the dictionary is the string for the menu item title. To get this string we get the bundle's infoDictionary, which is an NSDictionary that contains all of the information contained in the bundles Info.plist, and we use the same keys in Info.plist to access objects in the info dictionary. Therefore, we use the key IAFilterMenuItemTitle to get the NSString object containing the title we set for the plug-in.

And that's discoverPlug-ins.

Next we have setupFilterMenu. This method is pretty straightforward. The idea here is to enumerate the contents of the dictionary plug-ins and add menu items to the filter menu corresponding to each plug-in. Lets take a look to see how it looks:

- (void)setupFilterMenu { NSEnumerator *e = [plug-ins keyEnumerator]; NSString *name; while ( name = [e nextObject] ) { NSMenuItem *item = [[[NSMenuItem alloc] init] autorelease]; [item setTitle:name]; [item setTarget:self]; [item setAction:@selector(executeFilter:)]; [filterMenu insertItem:item atIndex:0]; } }

In this menu we went through each key in the dictionary, created a new NSMenuItem with that title, set the target of the menu item to self, the action to executeFilter:, and inserted the item into the filter menu.

The last method to put in IAPlug-inManager is executeFilter:. This is the action of every filter menu item. The purpose of executeFilter: is to get the correct filter instance from the plug-ins dictionary, using the menu item title as the key, and then to invoke in the MyDocument instance associated with the frontmost window the method filterImageUsingFilter: (which hasn't been defined yet). The method looks like the following:

- (void)executeFilter:(id)sender
    NSString *name = [sender title];
    IAFilter *filter = [plug-ins objectForKey:name];
    MyDocument *doc = [[NSDocumentController sharedDocumentController] currentDocument];
    [doc filterImageUsingFilter:filter];

In the method above we got the name of the filter from the title of the sender object, which will always be one of the menu items in the filter menu added by the method setupFilterMenu. Using that name we obtain the filter object corresponding to the menu item from dictionary plug-ins. Finally, we get the current document, and invoke the as-of-yet-undefined method filterImageUsingFilter: with the filter object passed as the parameter. The last piece of this puzzle is to add to MyDocument the following implementation of the method filterImageUsingFilter:

- (void)filterImageUsingFilter:(IAFilter *)filter
    activeImage = [[filter filterImage:activeImage] retain];
    [windowController setImageToDraw:activeImage];

Here we follow the pattern of our old method makeGrayscale: of passing the image assigned to activeImage to the filter through filterImage: and setting the activeImage to the returned image. We then tell the window controller for the document to update the displayed image to the new activeImage. And there you have it. Compile away, and play around with this to your heart's content.

One Last Thing

In the column before last, where we first learned about image filters, we created a Grayscale Filter. We did this by simply averaging together the magnitudes of the red, green, and blue components of the color into a single gray value. However, as many of you know and pointed out to me, the human eye is not equally sensitive to all colors of light. The correct thing to do is to do a weighted average of the three color components. So, where we had:

grayValue = ( red + green + blue ) / 3

equivalent (very nearly) to:

grayValue = 0.333*red + 0.333*green + 0.333*blue

we should have had:

grayValue = 0.222*red + 0.707*green + 0.071*blue.

So, while the details of the colorspace conversion were incorrect, the rest of our technique was correct. One link provided in the comments of that column containing lots of useful information about color that I found interesting is the Color space FAQ.

The End

What we have seen in today's column is how to create an plug-in architecture for an application. You can download the application here. For another perspective on this subject, have a look at this article by Rainer Brockerhoff. Where we created a public class for plug-in developers to subclass, he provides a formal protocol for developers to conform to.

Coming up in the next two columns we will take another detour from our Image App series to learn about networking in Cocoa: in the first we will look at the Foundation classes that let us use rendezvous, and in the second we will learn how to make two apps talk to each other using sockets. See you then!

Michael Beam is a software engineer in the energy industry specializing in seismic application development on Linux with C++ and Qt. He lives in Houston, Texas with his wife and son.

Read more Programming With Cocoa columns.

Return to the Mac DevCenter.

Copyright © 2009 O'Reilly Media, Inc.