- Author: David Gohara
- Web Site: http://gohara.wustl.edu
In this tutorial I'm going to cover the basics of setting up an Image Browser view in a Cocoa application. Image browser views are used in a number of Apple applications including Preview and iPhoto, and provide a nice way to quickly look at a set of images in an ordered way. Image Kit, is the framework that drives the IKImageBrowserView class, and is part of the Quartz umbrella framework. This tutorial requires Leopard, Xcode 3.0 and IB 3.0. Setting up and configuring an Image Browser view is actually quite simple, so rather than do a step by step, I'll provide the completed project and just highlight the relevant code sections for each type of functionality. We'll provide data to the view via Drag and Drop (but as you'll see from the code, the DnD can easily be replaced by any number of mechanisms). The Xcode project for the tutorial can be obtained here.
Task Breakdown
In order to get data to display in the view we have to do a little bit of work to massage the information into a state the view will understand. We can break these tasks down into four loose categories: delegate setup, view item setup, data source methods and drag and drop methods.
Delegate Setup
In IKBController.h you'll see we've declared two instance variables:
IBOutlet IKImageBrowserView * browserView; NSMutableArray * browserData;
Simple enough, one is our view the other is going to be the data source. The real setup occurs in IKBController.m in the awakeFromNib method:
//Allocate some space for the data source browserData = [[NSMutableArray alloc] initWithCapacity:10]; //Browser UI setup (can also be set in IB) [browserView setDelegate:self]; [browserView setDataSource:self]; [browserView setDraggingDestinationDelegate:self];
We do a couple of things here. First we set up our array that is going to serve as the data source. In this case an NSMutableArray so that we can simply load up objects into it. Next we take care of some configuration information for the delegate, the data source and the dragging destination delegate. Note that you can configure this in IB as well, but I moved it here for clarity in reconstructing the project. In all cases I'm setting the controller itself to take care of all of those responsibilities (that is, the controller implements all of the functionality to be a delegate, data source and dragging destination for the IKImageBrowserView. I'll discuss this more below).
Display Objects
Now that we have this setup we need to have a way to encapsulate information passed to the view into a form that it can easily render to screen. Each item in an IKImageBrowserView is referred to as an IKImageBrowserItem, which itself is just an informal protocol for how to handle information to be displayed in the view. In the project look at the file IKBBrowserItem. The item is simple, it has an NSImage object and an NSString that provides an ID for the item. Further down in the header you'll see three required declarations:
- (NSString *) imageUID; - (NSString *) imageRepresentationType; - (id) imageRepresentation;
In order for this object to conform to the informal protocol, these three methods must be implemented. This isn't complicated though, as you'll see in the file IKBBrowserItem.m. We start off the standard object initialization. Nothing fancy, we simply pass in an image and an ID. For the three required protocol methods:
- (NSString *) imageUID { return imageID; } - (NSString *) imageRepresentationType { return IKImageBrowserNSImageRepresentationType; } - (id) imageRepresentation { return image; }
This should be fairly self-explanatory. For imageUID, we simply return the imageID we created. We need to provide some information about the representation that is being sent to the view. Since it's an image we use IKImageBrowserNSImageRepresentationType. And of course, we need to provide the actual image representation, which is simply the NSImage object.
Data Source Setup
Back in our controller source file we need to implement two dat source methods. These are:
- (NSUInteger) numberOfItemsInImageBrowser:(IKImageBrowserView *) aBrowser { return [browserData count]; } - (id) imageBrowser:(IKImageBrowserView *) aBrowser itemAtIndex:(NSUInteger)index { return [browserData objectAtIndex:index]; }
The method numberOfItemsInImageBrowser is called by the view to its delegate (which in this case is the controller) and returns the number of data items in our browserData mutable array (our data source). The second method imageBrowser:itemAtIndex: simple returns the object at the specified index of the array. Again, this is called in sequence by the IKImageBrowserView for each element that will be displayed. You may be asking, “What IS that object that is returned?” Simple. It's one of the IKBBrowserItem objects we'll create when a Drag and Drop is completed. Let's cover that now.
Drag and Drop
For drag and drop to work, the drag and drop destination must conform to the NSDraggingDestination informal protocol. This is no different than most other types of drag and drop you'd perform (for example to a table view). Once you understand Drag and Drop in this context, you can implement it for almost any case. We are going to implement five of the eight methods needed to be a dragging destination (to be a dragging source there are other methods that need to be implemented):
- (unsigned int)draggingEntered:(id )sender; - (unsigned int)draggingUpdated:(id )sender; - (BOOL)prepareForDragOperation:(id )sender; - (BOOL)performDragOperation:(id )sender; - (void)concludeDragOperation:(id < NSDraggingInfo >)sender;
The majority of the work is done in the first, fourth and fifth methods. When a drag is initiated and the mouse falls within the bounds of the view (IKImageBrowserView) a message is sent to the delegate (the controller) notifying that dragging entered the view. At this point we have to decide what to do. If the pasteboard contains information that is consistent with the type of drag information we registered for (NSFilenamesPboardType), we return an NSDragOperationEvery. If the pasteboard doesn't contain the type of information we want, we return NSDragOperationNone and the drop won't be allowed to proceed. Note, that in a real world case, you may not want to accept any kind of drag, so check the docs to make sure you are returning the correct type. Each of these methods can be thought of as stages of execution. At each stage you can perform some operations on the information passed via sender to see if you want to continue or abort the drop. When the mouse is released (the drop occurs) performDragOperation is called. Here is where we do the real work:
- (BOOL)performDragOperation:(id )sender { //Get the files from the drop NSArray * files = [[sender draggingPasteboard] propertyListForType:NSFilenamesPboardType]; for(id file in files){ NSImage * image = [[NSWorkspace sharedWorkspace] iconForFile:file]; NSString * imageID = [file lastPathComponent]; IKBBrowserItem * item = [[IKBBrowserItem alloc] initWithImage:image imageID:imageID]; [browserData addObject:item]; } if([browserData count] > 0) return YES; return NO; }
Again, this is pretty straightforward. We get the list of filenames passed in by the sender. Then for each file in the array, we get the icon for the file (our NSImage) and set the imageID to the file name. We then create an IKBBrowserItem (our custom class that implements the IKBrowserItem informal protocol) and add it to our data source (browserData). If our array has objects in it we return YES, otherwise return NO. Pretty simple. Finally, concludeDragOperation is called. And in there we simple tell the browserView to reload the data from the data source (browserData).
- (void)concludeDragOperation:(id < NSDraggingInfo >)sender { [browserView reloadData]; }
That's it in terms of code!
Before heading over to IB let's recap.
1) We setup a controller to be a delegate for the view, a data source for the view and a delegate for drag and drop (a Superdelegate if you will, pun intended).
2) We create a custom subclass that acts as a container for the information to be displayed by the IKImageBrowserView. This container conforms to the IKBrowserItem informal protocol.
3) We implement the data source methods to provide the proper information to the view.
4) We implement drag and drop methods in order to extract information from the pasteboard and transform that information into a format that the view can understand.
Interface Builder
If you open the Resources triangle, you'll see MainMenu.nib. Double-click it to launch IB. Once in IB you'll see our Controller object (IKBController) and the Window object. If it's not open already open the Window object. There you'll see an IKImageBrowserView palette item. A connection has been made from the IKBController TO the IKImageBrowserView (browserView). That's it. All of the other connections were done in code. However, you could also rig up the delegate, data source and drag delegate outlets in IB by initiating connections FROM the IKImageBrowserView TO the IKBController.
Build and Go
Back in Xcode press Build and Go. When the application finishes building and launches, try dragging some files to view and then let go. It should display icons for each of the files you dropped. Select some other files and drag those over, and you'll see they get added. Note, that we didn't add any logic to control duplicate items, so if you drag the same items over and over, they'll keep getting added to the view.
Also note that you cannot delete items with the current implementation and you can't use the view as dragging source in its current form. Well, that's it for Part I. In Part II, I'll extend this project to show how to expose certain features in Leopard we aren't supposed to use. Why would we want to do this? Because it's fun. Stay tuned.