Learning Cocoa with Objective-C/Miscellaneous Topics/Bundles and Resources

From WikiContent

Jump to: navigation, search
Learning Cocoa with Objective-C

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

Contents

Peeking Inside Bundles

To get a better idea of how bundles go together, let's take a look inside a bundle that is already on your system.

  1. Use the Finder to browse to the iPhoto application, located in the /Applications folder.
  2. 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.

Figure 13-1. Looking inside an application bundle

Looking inside an application bundle

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.

Figure 13-2. A minimal bundle

A minimal bundle

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

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.

Bundle Directories

In addition to the Info.plistand PkgInfofiles, the following directories can appear in the Contentsdirectory:

MacOS
Contains the actual executable code for an application or plug-in.
Resources
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.
Frameworks
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.
Shared Frameworks
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.
Shared Support
Contains helper applications, assistants, and other tools that may be used by an application.

Using Bundles

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.

  1. Create a new Cocoa Application project in Project Builder (File → New Project → Application → Cocoa Application) named "Image Bundle", and save it in your ~/LearningCocoafolder.
  2. Add some image files to the project using the Add Files command (Project → Add Files). Navigate to the /Library/Desktop Pictures/Abstractfolder, select all the JPEG images (named Abstract 1-8.jpg), and click the Add button.

    Note

    To select multiple files, as required in step 2, you can[[Image:Learning Cocoa with Objective-C_I_2_tt493.png|]]-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.

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

    Figure 13-3. Adding files to the project

    Adding files to the project

  4. Save the project (File → Save, or [[Image:Learning Cocoa with Objective-C_I_2_tt495.png|]]-S).
  5. Next, open the MainMenu.nib file in Interface Builder.
  6. 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.

    Figure 13-4. Adding an Image View to our application window

    Adding an Image View to our application window

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

    Tip

    You can also create a subclass by locating NSObject in the Classes pane and hitting the Return key. A new subclass will be created, and all you need to do is enter a new name for the subclass.

  8. Create an outlet named imageView on the Controller object using the Inspector, as shown in Figure 13-5. Type the outlet as NSImageView.

    Figure 13-5. Adding an imageView outlet to the Controller class

    Adding an imageView outlet to the Controller class

  9. Create the source files for the Controller class (Classes → Create Files for Controller, or Option-[[Image:Learning Cocoa with Objective-C_I_2_tt498.png|]]-F).
  10. Instantiate the Controller class (Classes → Instantiate Controller, or Option-[[Image:Learning Cocoa with Objective-C_I_2_tt499.png|]]-I).
  11. Control-drag a connection from the Controller object to the image view. Hook up the connection to the imageView outlet in the Info window.
  12. Save the nib file ([[Image:Learning Cocoa with Objective-C_I_2_tt500.png|]]-S), and return to Project Builder.
  13. 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:

    1. Gets a reference to the bundle object from which this application was loaded.
    2. 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
      
    3. Creates an NSImage object using the file in our application bundle.
    4. Tells the imageView of our application interface to display the image.
    5. Releases the image, now that we are done with it and the image view has it.

  14. Build and run ([[Image:Learning Cocoa with Objective-C_I_2_tt503.png|]]-R) the application. The application should look like Figure 13-6.

    Figure 13-6. Image Bundle running and showing Abstract 1.jpg

    Image Bundle running and showing Abstract 1.jpg

  15. Open the Products group in the Groups & Files pane, and examine the Image Bundle.appitem, 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.

    Figure 13-7. Examining the built application bundle for Image Bundle

    Examining the built application bundle for Image 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.

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

  2. Save ([[Image:Learning Cocoa with Objective-C_I_2_tt507.png|]]-S) the Controller.h file.
  3. Open the MainMenu.nib file in Interface Builder.
  4. 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.
  5. Add a new button, named Next, to our interface, as shown in Figure 13-8.

    Figure 13-8. Adding a Next button to our interface

    Adding a Next button to our interface

  6. Connect the Next button to the nextImage: action method on the Controller instance object.
  7. Save the nib, and return to Project Builder.
  8. Modify the awakeFromNib method in Controller.mto 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:

    1. 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.
    2. Retains the reference to the imagePaths array so that it doesn't disappear out from under us.
    3. Sets the currentImage counter to 0.
    4. Creates a new NSImage object using the first path of the array of paths we obtained in line a.
  9. Add the nextImage: action method to Controller.mas 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:

    1. Increments the image at which we want to look by 1.
    2. Checks to see if we've incremented the counter past the number of images we have. If so, we reset the counter to 0.
    3. Creates an NSImage object using the path at the current index.
    4. Sets the image view to display the new image.
  10. Save the project (File → Save, or [[Image:Learning Cocoa with Objective-C_I_2_tt511.png|]]-S).
  11. Build and run ([[Image:Learning Cocoa with Objective-C_I_2_tt512.png|]]-R) the application. You should now be able to step through the sequence of images in the bundle.

A Performance Diversion

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:

  1. Modify the Controller.hfile 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.

  2. 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:

    1. Loads the paths to all the JPEG images in the bundle into an array
    2. Creates a mutable array to the location where the images will be stored
    3. Loops through the image paths and creates a new NSImage object with each path
    4. Sets the image view to display the first image
  3. Now, modify the nextImage: method in Controller.mto 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:

    1. Checks to see if we have incremented the counter past the number of images loaded. If so, it resets the counter to 0.
    2. Sets the image displayed into the image view to the next image.
  4. Save the project (File → Save, or [[Image:Learning Cocoa with Objective-C_I_2_tt516.png|]]-S).
  5. Build and run ([[Image:Learning Cocoa with Objective-C_I_2_tt517.png|]]-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.

Tip

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.

Exercises

  1. Add a Previous button to the Image Bundle application.
  2. Add keyboard shortcuts for both the Next and Previous buttons.
Personal tools