MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Build an eDoc Reader for your iPod

by Matthew Russell
12/14/2004

Apple's iPod has caught the world by storm and is emerging into a really handy gadget that does a lot more than play music. Although eBook reading isn't synonymous with iPod yet, it might be once you've completed this project.

This article is the first in a series that walks you through the process of developing a Cocoa application that allows you to read large text documents, PDF files, and other electronic books on your 3G iPod or newer. Following along with this series highlights a number of valuable concepts such as text wrangling, interfacing to the user defaults system, incorporating existing open source software as a part of your own project, and tackling the Cocoa-Java bridge.

The User Interface

For development, we'll use the latest version of Xcode--the IDE of choice for any Cocoa application. If you don't already have the latest version of Xcode, you can download it from Apple Developer Connection. You'll have to register, but it's free and worth the trouble. As with any other Cocoa project, open up Xcode, create a new project, and choose a new "Cocoa Application." Name the project "Pod Reader" and place it in whatever location you like. In Xcode, expand the "NIB files" folder at the very bottom of the leftmost pane to reveal "MainMenu.nib." Double click on "MainMenu.nib" to fire up IB (Interface Builder).

We'll follow the Model View Controller (MVC) paradigm for application design. MVC is the defacto design standard for Cocoa applications. The Model comprises custom classes that perform the bulk of the labor, the View encapsulates the GUI, and the Controller handles the flow of data between our model and our view. Normally, the best models and controls in the view are designed to be general and reusable. Controllers are typically application specific by necessity.

The View

Using the IB panel with the controls on it, try and make your application look like the finished product shown below. First, resize the rather large window panel IB opens up for you to a more suitable size. Once it's resized, drag the appropriate AppKit framework controls onto the NSWindow panel. In all, you should need four NSTextFields (one of them is customized as a "System Font Text" label), three NSButtons, and an NSProgressIndicator.


The finished product. You specify a document and a destination directory (the iPod), designate a value to separate sections of a document on (such as "chapter" or "scene"), click on "Copy It," and the document is "intelligently" chunked into 4KB files you can read on your iPod. (Currently, the iPod will not display files larger than 4KB.)

The NSTextField that has the label "System Font Text" on it looks different from the other three, but it is actually the very same control. A few of its attributes are set to make it a label as a convenience to you. You can check it out for yourself and learn a lot more about the NSTextField by doing a comparison of the two. Select "Tools -> Show Info" from the menu and then select the "Attributes" menu at the top while the NSTextField is selected. You'll want to get familiar with this Info window and all that it has to offer you.



Drag controls onto your window using the controls palette. Controls are organized into groups designated by the topmost row. As you layout your design, use IB's guidelines for placing the widgets. IB does a nice job of helping you to line them up in accordance with human interface guidelines via its markup (dashed lines that appear) inside the window.


If you haven't worked in IB very much before, you might like to know that selecting and then clicking controls like NSButton a second time makes their label editable. You might also like to note that opening the Info on "Window" in the "Instances" tab of MainMenu.nib allows you to give the window a title and allows you to disable resizing by unchecking the "Resize" control.

Customizing the View

Your fingers are itching to write code at this point, but there's still a few things we can do first. As you recall, the controller handles data flow in the application. Its accomplishes this task through "outlets" and "actions." Outlets are nothing more than pointers to objects in the view. For example, we may want to modify the value of an NSTextField in our view, so we use its outlet in the controller to pass it a message. Actions are very similar; they are pointers to methods that are called in response to events that occur in the view, such as pressing an NSButton.

A few outlets we can take care of right away are ones that control the flow of tabbing. The NSTextFields directly above the "Source File" and "Destination" buttons should not be editable or selectable, so open their Info window and uncheck the "Editable" and "Selectable" options near the bottom. Also, uncheck "Enabled" for the "Copy It" button. We'll programmatically enable it in the application once certain conditions are met.

The NSTextField acting as a label as well as the NSTextFields just above the "Source File" and "Destination" buttons cannot be tabbed into, so that takes care of them. For the remaining controls, we'd like for the tabbing flow to proceed from top to bottom: from the "Source File" button, to the "Destination" button, to the text field we can type a "Separator" value into, and finally to the "Copy It" button.

To set the tabbing from "Source File" to "Destination," hold down the control button while the "Source File" button is selected, drag down to "Destination," and once "Destination" is marked up, release your mouse button. You should see a line drawn on your screen connecting them, and the Info window should open.

Select "nextKeyView" in the outlets tab and click "Connect." Repeat the same process to set the tabbing for the remaining controls, finishing up the loop by wrapping the "Copy It" button back up to the "Source File" button. To set the outlet that starts keyboard control with the "Source File" button as soon as the application starts, we'll designate the "Source File" button as the Initial First Responder. Setting this outlet is just like the others we set; control click from "Window" in the "Classes" tab of MainMenu.nib to the "Source File" button. Select "initialFirstResponder" once the Info window opens, and then click on "Connect."



Create outlets that handle the flow of tabbing and set the first responder status.

Creating the AppController Class

We're still not ready to write any code, but we are getting closer. In IB, click on the "Classes" tab of the MainMenu.nib palette. In the root of the browser, select "NSObject" and then choose "Classes -> Subclass NSObject" on the main menu. A new subclass of NSObject appears in the next pane of the browser; name it "AppController."


Create the AppController class by subclassing NSObject.

To specify the controller's "outlets" and "actions," select "AppController" in the MainMenu.nib palette, and then press "1" while holding down the Command key. The "AppController Class Info" window should appear. With the "Outlets" tab selected, click on the "Add" button to create an outlet. Name this outlet "copyButton," and change its corresponding popup button value from "id" to "NSButton." This change statically types "copyButton" as an NSButton. Statically typing objects can make debugging easier because it allows the compiler to provide better feedback. Repeat this process and create "destButton" and "sourceButton" outlets of type NSButton.

Next, create three outlets of type NSTextField and call them "destDir," "sourceFile," and "separatorValue." Finally, create a "progressIndicator" outlet of type NSProgressIndicator. You can go to "Help -> Documentation" in Xcode and type "NSProgressIndicator" in the search box to look up more information about NSProgressIndicator or any other class in this article. In the "Actions" tab, create two actions in an analogous manner to the outlets. Name the actions "copyIt" and "openFileDialog." Xcode inserts colons after their names for you. Your final actions and outlets should look like the ones below.



Outlets and Actions for AppController in IB.

Generating Files for AppController

With all of that behind us, we can now generate the controller's code and instantiate it. Select "AppController" from the "Classes" tab in MainMenu.nib, and then go to the "Classes" menu at the top of the screen and choose "Create Files for AppController." Choose the default options that appear to generate the files and insert them into the current target.

Back in Xcode, you can now expand the "Other Sources" folder in the leftmost pane to reveal the AppController.m and AppController.h files. Drag these two files to the "Classes" folder, and inspect them--trying to draw parallels to the work you accomplished in IB. In AppController.h, you should see seven pointers to objects prefaced by the "IBOutlet" macro and two actions prefaced by the "IBAction" macro. Your AppController.m file should have the shells for the two actions. For each of these actions, insert the following log message.

NSLog(@"This %@ button works!", sender);

Instantiating the Controller

We now have a view along with a controller class and its source files, but the view and control are still disjointed. To bridge the gap between them, we need to instantiate the controller in IB. To instantiate it, select "AppController" and then "Instantiate AppController" from the main menu. You should see a blue cube appear in the "Instances" tab of IB with the appropriate label. At this point, we'll connect all of the outlets and actions specified in the controller's code with the controls on our view.

Direction of Connections

When you control-dragged to specify the tabbing outlets earlier, you noticed that it was in a directional manner. The "Source File" button passed control to the "Destination" button, so we dragged FROM "Source File" TO "Destination." In a similar manner, we'll always specify our outlets and actions FROM the sender TO the receiver. Take a moment to let that soak in. It's really important to understand the direction of the connections.


Drag FROM "AppController" TO the NSTextField when setting its outlet.

First we'll connect some of the outlets. Control drag FROM the blue cube entitled "AppController" in the MainMenu.nib palette TO each of the NSTextFields except for the label. You'll see the normal markup and the Info window appear.

Related Reading

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

Under the "Outlets" tab, choose the corresponding outlet. These connections allow the controller to address the NSTextFields in such ways as getting and setting their value. To set the actions for each of the buttons, control drag FROM each of the three NSButtons TO "AppController" in MainMenu.nib. In the Info window that appears, select the "Actions" tab, and connect to the appropriate action by clicking on "Connect." The "openFileDialog" action is used for both the "Source File" and "Destination" buttons.

At this point, you should notice that there are still three outlets that are not connected. To create them, control drag FROM the "AppController" TO each of the buttons and create the connection just like you did for text fields. These outlets are being set separately from the other outlets in order to underscore the difference between outlets and actions. The first set of outlets allows our controller to "know about" the NSTextField objects. The set of actions allows our NSButton objects to "trigger" methods in response to a click.

This final set of outlets allows our controller to "know" about our buttons. Although buttons normally don't do things like change values during an application, we do need to know which button was clicked that triggered an action. For example, the "openFileDialog:" action occurs in response to clicks from both the "Source File" and the "Destination" buttons. In AppController.m, You'll notice that actions pass in the identity of the control that triggered them. In our application, "openFileDialog:" responds differently, depending on which control triggered it.

Build and Run It

You should now be able to run what you have so far in Xcode. Keyboard control should start with the "Source File" button, and you should be able to tab through the buttons and the separator text field in the correct order. When you click on one of the two buttons that are enabled, you should see the correct log message appear. Additionally, you should not be able to type into the text fields just above the "Source File" or "Destination" buttons. If you have issues, start the troubleshooting process with the actions and outlets.

Now is a good time to handle that disabled "Copy It" button. The idea is that no copy can take place until after the user has specified both a source file and a destination directory (the iPod). By our default settings in IB, "Copy It" is disabled, but we'll programatically enable it via its outlet in the controller if both the sourceFile and destDir NSTextFields are not empty. Take a moment to look up NSTextField and NSButton in Xcode's documentation the same way you did for NSProgressIndicator for more insight into how we'll do this.

Ten Minutes Later: In your pursuit of knowledge, you noticed that both NSTextField and NSButton inherit from NSControl, so you looked back to NSControl to find the stringValue: and setEnabled: methods. Thus, we can programmatically enable the copy button as so:


if (! 
	(([[sourceFile stringValue] isEqualToString:@""]) || 
	([[destDir stringValue] isEqualToString:@""]))
	) {
	[copyButton setEnabled:YES];
	}

Programmatically using outlets in the controller to change the view.

Before I can leave you to do some tinkering on your own, we'll need to declare an array and override two methods inherited from NSObject: "init:" and "dealloc:." The "init:" method provides a standard location to initialize objects that require dynamic memory. The "dealloc:" method is the standard location to clean up memory declared in "init." If you"re not familiar with Cocoa memory management, you should have a look at Introduction to Memory Management before next time. Copy the "init:" and "alloc" given below into your project and take a look at what each line of code accomplishes. Comments to guide you are inline.


////////////////////////////////////////////////////////////////////////////
//In AppController.h
////////////////////////////////////////////////////////////////////////////
//Declare this array in AppController.h
//Make sure it's inside the curly brackets
NSArray* typesArray;

//Declare these methods in AppController.h
//Make sure they're outside of the curly brackets
- (id)init;
- (void)dealloc;

////////////////////////////////////////////////////////////////////////////
//In AppController.m
////////////////////////////////////////////////////////////////////////////
- (id)init {
	//call parent"s init
	[super init];

	//create an array and retain it. Otherwise, it is placed in the
	//autorelease pool and released as soon as this method
	//ends because it's created with a convenience constructor

	//these string values specify the types of files that will be available
	//to select in an NSOpenPanel when the user triggers "openFileDialog"
	typesArray = [[NSArray arrayWithObjects: @"txt", @"pdf", nil] retain];

	return self;
}

- (void)dealloc {
	//clean up that memory from the retain call in init
	[typesArray release];

	//call parent"s dealloc
	[super dealloc];
}

To stretch your mind, I'm giving you the code for the "openFileDialog:" method that we'll walk through next time. This method gives you the last big chunk of logic that you need to complete the user interface portion. Specifically, do these things to get rolling:


- (IBAction)openFileDialog:(id)sender { 
	//Research this control
	NSOpenPanel* openPanel; 
	
	if (0 == [typesArray count]) { 
		typesArray = nil; //allow any type
	} 

	//configure the open panel 
	openPanel = [NSOpenPanel openPanel]; 
	[openPanel setAllowsMultipleSelection:NO]; 
	[openPanel setTreatsFilePackagesAsDirectories:NO]; 
	[openPanel setResolvesAliases:YES]; 
		
	if ([sender isEqualTo:sourceButton]) {
		[openPanel setCanChooseDirectories:NO]; 
		[openPanel setCanChooseFiles:YES]; 
		[openPanel setTitle:@"Source File"];
		
		//a good method to research in the NSString class
		[openPanel setDirectory:[@"~" stringByExpandingTildeInPath]]; 
	}
	else {
		[openPanel setCanChooseDirectories:YES]; 

		//where does this method come from? It's not in the
		//documentation for NSOpenPanel. Hint: inheritance.
		[openPanel setCanCreateDirectories:YES];

		[openPanel setCanChooseFiles:NO]; 
		[openPanel setTitle:@"Destination Directory"];
		
		//the iPod should be in this directory, if mounted
		[openPanel setDirectory:@"/Volumes"]; 
	}
		
	if (NSOKButton == [openPanel runModalForTypes:typesArray]) { 
		NSArray* selection = [openPanel filenames];
		
		if ([sender isEqualTo:sourceButton]) {
			[sourceFile setStringValue:[selection lastObject]];
		}
		else {
			[destDir setStringValue:[selection lastObject]];
		}
	} 
	
	if  (! 
			(([[sourceFile stringValue] isEqualToString:@""]) || 
		    ([[destDir stringValue] isEqualToString:@""]))
		 ) {
		[copyButton setEnabled:YES];
	}	
} 

Next Time

Next time, we'll briefly review some of the "openFileDialog:" code as well as touch on memory management and how it applies in a very limited sense to this project. We'll also implement the bulk of our model--a parser that intelligently segments our documents into logical sections. When we're finished, the project will be fully functional for plain text files. In the final installment, we'll complete the encore portion by using the Cocoa-Java bridge to incorporate an existing open source project that allows us to extract and chunk the text from PDF documents.

Matthew Russell is a computer scientist from middle Tennessee; and serves Digital Reasoning Systems as the Director of Advanced Technology. Hacking and writing are two activities essential to his renaissance man regimen.


Return to the Mac DevCenter

Copyright © 2009 O'Reilly Media, Inc.