Even though it may look like a single file in the Finder, a Cocoa application is actually a collection of files in a special directory structure known as a bundle. Bundle directories in the filesystem have a special significance that the Finder understands and that allows users to treat applications, as well as other types of bundles, as a single entity. This allows users to install an application simply by dragging it from a CD image and relocate it by dragging it around the filesystem.
There are three general types of bundles:
Application bundles contain an executable and all its related resources, such as nib files, image files, and localized strings. For example, most of the applications installed in the /Applications folder are application bundles.
Plug-in bundles provide code that extends or enhances the functionality of a host application in some way. They plug into some kind of architecture provided by the host application. An example of a plug-in bundle is the screensaver modules installed in the /System/Library/Screen Savers folder. Each of these bundles is used by the screensaver system (whose control panel is in the System Preferences).
Framework bundles contain dynamic shared libraries, as well as header files, images, and documentation. For example, the two Cocoa frameworks, Foundation and AppKit, are packaged as framework bundles in the /System/Library /Frameworks folder. Framework bundles differ from other bundles in that the Finder allows you to browse their contents. This allows you to browse the contents of a framework easily.
The essence of a bundle is that it pulls together a set of resources into a single package. This mechanism works on a variety of filesystems, from the dual fork-based HFS+ filesystem that Mac OS X prefers to single-fork SMB and NFS volumes that might be mounted from Windows or Unix servers. In this chapter, we take a look at application and other bundles and how to manage and obtain resources from them.
To get a better idea of how bundles go together, let’s take a look inside a bundle that is already on your system.
Use the Finder to browse to the iPhoto application, located in the /Applications folder.
Control-click on iPhoto. Select Show Package Contents from the context menu. A Finder window rooted at the directory in which iPhoto is located will open. You can use this Finder window to browse around the internals of the application. Figure 13-1 shows a column view of the application.
All of the contents of a bundle exist in the aptly named Contents directory. At a very minimum, a bundle consists of two files—Info.plist and PkgInfo —located in the Contents directory, as shown in Figure 13-2.
The Info.plist file is an XML-based property list file that specifies the following:
The name of the main executable for the bundle
Version info
Type and creator codes
Document types that the application handles and what role (editor or view) the application plays for a document type
Application and document icons
The kinds of data that the application can handle via the pasteboard
Application-specific attribute information
When we used Project Builder in Chapter 11 to manipulate the document-type application settings for Simple Edit and RTF Edit, those settings were automatically made into the Info.plist file.
PkgInfo contains only the type and creator codes for the application. This info is redundant with that in the Info.plist, but it is held separately so the Finder can use this information more efficiently.
In addition to the Info.plist and PkgInfo files, the following directories can appear in the Contents directory:
Contains the actual executable code for an application or plug-in.
Contains the various resources an application uses. These resources include nib files, images, localized strings, and icon files. Older Mac OS applications stored these resources in the resource fork of the application’s executable file.
Contains frameworks on which the application depends. These frameworks will always be used by an application, even if newer versions exist on the users’ system. This ensures that a specific version of a framework, which you need for your application, is always used.
Contains frameworks that will be used by the application unless a newer version of the framework exists on the local system. These frameworks can be superceded by shared frameworks in other applications, allowing programs to take advantage of the latest code.
Contains helper applications, assistants, and other tools that may be used by an application.
You can obtain the contents of bundles, even the application bundle
from which your application is running, by using the
NSBundle
class. This class provides methods to
obtain the paths to resources within your application, as well as
methods to load and link executable code that is located in a bundle.
To demonstrate working with bundles, we will build a simple application that loads an image into an image view.
Create a new Cocoa Application project in Project Builder (File → New Project → Application → Cocoa Application) named “Image Bundle”, and save it in your ~/LearningCocoa folder.
Add some image files to the project using the Add Files command (Project → Add Files). Navigate to the /Library/Desktop Pictures/Abstract folder, select all the JPEG images (named Abstract 1-8.jpg), and click the Add button.
To select multiple files, as required in step 2, you can
-click each image file and then click the Add button to load all of the images at once. This method of selecting files is particularly helpful when you want to pick and choose the files, rather than selecting them all.
When adding files, Project Builder also allows you to select a directory and click the Add button.
In the next sheet that drops down, make sure that the Copy items checkbox is clicked, as shown in Figure 13-3, and click the Add button.
Save the project (File → Save, or
-S).
Next, open the MainMenu.nib file in Interface Builder.
Drag an image view (NSImageView
) object from the
Cocoa-Other views palette into the main application window, and
resize it so that it occupies the entire window, as shown as Figure 13-4. Set the Autosizing attributes so that the
view will expand and contract if the user resizes the window.
Create a subclass of NSObject
in Interface
Builder. To do this, click on the Classes tab of the MainMenu.nib
window, find and Control-click on NSObject, and then select Subclass
NSObject from the pop-up menu. Name the subclass
“Controller”.
Create an outlet named imageView
on the Controller
object using the Inspector, as shown in Figure 13-5.
Type the outlet as NSImageView
.
Create the source files for the Controller
class
(Classes → Create Files for Controller, or
Option-
-F).
Instantiate the Controller
class (Classes
→ Instantiate Controller, or
Option-
-I).
Control-drag a connection from the Controller
object to the image view. Hook up the connection to the
imageView
outlet in the Info window.
Save the nib file (
-S), and return to Project Builder.
Add an awakeFromNib
method to the
Controller.m file as follows:
#import "Controller.h" @implementation Controller - (void)awakeFromNib { NSBundle * mainBundle = [NSBundle mainBundle]; // a NSString * path = [mainBundle pathForResource:@"Abstract 1" // b ofType:@"jpg"]; NSImage * image = [[NSImage alloc]initWithContentsOfFile:path]; // c [imageView setImage:image]; // d [image release]; // e } @end
The code we added performs the following tasks:
Gets a reference to the bundle object from which this application was loaded.
Uses the pathForResource:ofType:
method of the
NSBundle
class to look up the path of the
Abstract 1.jpg file in the application bundle.
If we were to print out the path that results, it would be as
follows:
~/LearningCocoa/Image Bundle/build/Image Bundle.app/Contents/Resources/Abstract 1.jpg
Creates an NSImage
object using the file in our
application bundle.
Tells the imageView
of our application interface
to display the image.
Releases the image, now that we are done with it and the image view has it.
Build and run (
-R) the application. The application should look like Figure 13-6.
Open the Products group in the Groups & Files pane, and examine the Image Bundle.app item, shown in Figure 13-7. This is the built application bundle and all of the resources inside of it. During the build process, Project Builder automatically moves the image files that we added to the project into the Resources directory of the application bundle.
Instead of just obtaining specific files from the application bundle, we can get all of the resources of a particular type. To illustrate this, we’ll add a Next button to the application, which will iterate over the set of images in our application.
Edit the Controller.h file, and add the following code:
#import <Cocoa/Cocoa.h> @interface Controller : NSObject { IBOutlet NSImageView *imageView; NSArray * imagePaths; int currentImage; } - (IBAction)nextImage:(id)sender; @end
This allows us to keep track of the paths to all the images in the bundle, as well as keep a count of what image we’re showing. In addition, it adds the method declaration for the action method.
Save (
-S) the Controller.h file.
Open the MainMenu.nib file in Interface Builder.
Click on the Classes tab of the MainMenu.nib window; find the
Controller
class, and then reread the source file
(Classes → Read Controller.h) so that Interface
Builder can pick up the new action method.
Add a new button, named Next, to our interface, as shown in Figure 13-8.
Connect the Next button to the nextImage:
action
method on the Controller
instance object.
Save the nib, and return to Project Builder.
Modify the awakeFromNib
method in
Controller.m to match the following code. Note
that we have changed lines b and c from the previous implementation
of this method.
- (void)awakeFromNib { NSBundle * mainBundle = [NSBundle mainBundle]; imagePaths = [mainBundle pathsForResourcesOfType:@"jpg" // a inDirectory:nil]; [imagePaths retain]; // b currentImage = 0; // c NSImage * image = [[NSImage alloc]initWithContentsOfFile: // d [imagePaths objectAtIndex:currentImage]]; [imageView setImage:image]; [image release]; }
This code performs the following tasks:
Obtains an array of paths for all the JPEG files in our application.
The nil
argument tells the method to look in the
default Resources directory. If the images were located in a
subdirectory of the bundle, we could specify that subdirectory here
as well.
Retains the reference to the imagePaths
array so
that it doesn’t disappear out from under us.
Sets the currentImage
counter to 0.
Creates a new NSImage
object using the first path
of the array of paths we obtained in line a.
Add the nextImage:
action method to
Controller.m as follows:
- (IBAction)nextImage:(id)sender { currentImage++; // a if (currentImage == [imagePaths count]) { // b currentImage = 0; } NSImage * image = [[NSImage alloc]initWithContentsOfFile: // c [imagePaths objectAtIndex:currentImage]]; [imageView setImage:image]; // d [image release]; }
The code we added performs the following tasks:
Increments the image at which we want to look by 1.
Checks to see if we’ve incremented the counter past the number of images we have. If so, we reset the counter to 0.
Creates an NSImage
object using the path at the
current index.
Sets the image view to display the new image.
Save the project (File → Save, or
-S).
Build and run (
-R) the application. You should now be able to step through the sequence of images in the bundle.
When you run the Image Bundle application, you’ll notice that loading the next image isn’t exactly snappy. Even on a PowerBook G4, there is a notable lag as each image loads into the window. This is because we are going back to the filesystem and forcing Cocoa to reload the image each time we click on the Next button. We can fix this performance problem by preloading the images. The following steps will modify the code:
Modify the Controller.h file to match the following code:
#import <Cocoa/Cocoa.h>
@interface Controller : NSObject
{
IBOutlet NSImageView *imageView;
NSMutableArray * images;
int currentImage;
}
- (IBAction)nextImage:(id)sender;
@end
Here, we’ve changed the name of the array to indicate that we will hold references to images, not to the paths at which the images are located.
Modify the awakeFromNib
method in
Controller.m to match the following code. We are
changing almost every line of code, so be careful here.
- (void)awakeFromNib { NSBundle * mainBundle = [NSBundle mainBundle]; NSArray * imagePaths = [mainBundle pathsForResourcesOfType:@"jpg" // a inDirectory:nil]; images = [[NSMutableArray alloc] init]; // b int count = [imagePaths count]; int i; for (i = 0; i < count; i++) { // c NSImage * image = [[NSImage alloc] initWithContentsOfFile: [imagePaths objectAtIndex:i]]; [images addObject:image]; [image release] } currentImage = 0; [imageView setImage:[images objectAtIndex:currentImage]]; // d }
This code does the following things:
Loads the paths to all the JPEG images in the bundle into an array
Creates a mutable array to the location where the images will be stored
Loops through the image paths and creates a new
NSImage
object with each path
Sets the image view to display the first image
Now, modify the nextImage:
method in
Controller.m to match the following code. This
method actually gets simpler as a result of the work that we did in
the awakeFromNib
method.
- (IBAction)nextImage:(id)sender { currentImage++; if (currentImage == [images count]) { // a currentImage = 0; } [imageView setImage:[images objectAtIndex:currentImage]]; // b }
The code we added does the following things:
Checks to see if we have incremented the counter past the number of
images loaded. If so, it resets the counter to 0
.
Sets the image displayed into the image view to the next image.
Save the project (File → Save, or
-S).
Build and run (
-R) the project. You’ll notice that it takes longer for the application to launch than it did before, but switching between images is now much quicker. As with most performance optimizations, the price of loading the images has to be paid somewhere; it’s just a matter of when the price is paid.
The real answer to our performance problem is a background thread that loads the images after the first image is loaded and displayed. Doing this would move the price of loading the images to after the application was already displayed, when the user wouldn’t care. However, using threads is not easy and is an advanced topic beyond the scope of this book.