oreilly.comSafari Books Online.Conferences.


AddThis Social Bookmark Button

Build an eDoc Reader for your iPod, Part 2
Pages: 1, 2

In order to set these values when the application starts, we'll need to implement the awakeFromNib protocol, which you can get more info on by looking up NSNibAwaking in Xcode's help. Essentially, all objects like our controls in the view are guaranteed to have been initialized by the time this method is called, so we can safely set their values in awakeFromNib.

Wow. With all of that behind us, replace your AppController files based on our discussion with these: AppController.h and AppController.m. Then build and run your project. Try out a book from Project Gutenberg . They have a rather long preamble of "small print," and I find that removing that portion results in better overall "chunking" into logical sections. For homework, you might try to use some of NSString's methods to automate that process.

Use the Notes folder of your iPod as the destination directory, but ensure that your iPod is configured for disk storage by using these instructions if you haven't already, or else you won't be able to get to the Notes folder. As a sanity check, you should be able to type cd /Volumes/your iPod's name/Notes into Terminal and be in your Notes folder without any problems. Once you've chunked a few books, come back and we'll review the interesting parts of the code. Be aware that your Notes folder can hold, at a maximum, 1,000 note files. I recommend creating subfolders for each document to better organize your readings.

Shots of your PodReader in action. Simply click on the link to go to the next "page."

/* AppController.h */

#import <Cocoa/Cocoa.h>
#import "TextChunker.h"

@interface AppController : NSObject {
    IBOutlet NSTextField*		destDir;
    IBOutlet NSTextField*		sourceFile;
	IBOutlet NSButton*		sourceButton;
	IBOutlet NSButton*		destButton;
	IBOutlet NSButton*		copyButton;
	IBOutlet NSTextField*		separatorValue;
	IBOutlet NSProgressIndicator*	progressIndicator;
	TextChunker* chunker;
	NSArray* typesArray;
	NSUserDefaults* userDefaults;
	NSFileManager* fileManager;

- (id)init;
- (void)awakeFromNib;
- (IBAction)copyIt:(id)sender;
- (IBAction)openFileDialog:(id)sender;
- (void)dealloc;


Here in the AppController header file, we #import the TextChunker header file and declare that we'll be using it, along with the NSUserDefaults class and the NSFileManager class. We also indicate that we'll implement the awakeFromNib protocol and the copyIt: action. In AppController.m, we declare these methods in the same order for consistency and ease of looking things up.

* AppController.m */
#import "AppController.h"

NSString* LAST_DEST_DIR = @"LastDestDir";
NSString* LAST_SOURCE_FILE = @"LastSourceFile";
NSString* LAST_SEPARATOR = @"LastSeparator";
@implementation AppController

- (id)init {
        [super init];
        typesArray = [[NSArray arrayWithObjects: @"txt", @"pdf", nil] retain];
        chunker = [[TextChunker alloc] init];           
        fileManager =  [[NSFileManager alloc] init];
        userDefaults = [NSUserDefaults standardUserDefaults];
        return self;

- (void)awakeFromNib {
        //the text fields are set up to be blank by default in the nib
        if ((nil != [userDefaults objectForKey:LAST_DEST_DIR]) &&
                 [fileManager fileExistsAtPath:
                       [userDefaults objectForKey:LAST_DEST_DIR]]) 
                 [destDir setStringValue:
                     [userDefaults stringForKey:LAST_DEST_DIR]];
        if ((nil != [userDefaults objectForKey:LAST_SOURCE_FILE]) &&
                [fileManager fileExistsAtPath:
                    [userDefaults objectForKey:LAST_SOURCE_FILE]]) 
                [sourceFile setStringValue:
                    [userDefaults stringForKey:LAST_SOURCE_FILE]];
        if (nil != [userDefaults objectForKey:LAST_SEPARATOR]) 
                [separatorValue setStringValue:
                    [userDefaults stringForKey:LAST_SEPARATOR]];

        //the copy button is set to be disabled by default in the
        //nib and the text fields are set to be blank by default
        if  (! 
                 (([[sourceFile stringValue] isEqualToString:@""]) || 
                  ([[destDir stringValue] isEqualToString:@""]))
                [copyButton setEnabled:YES];
        [progressIndicator setUsesThreadedAnimation:YES];

The init: method declares memory for objects just as we've discussed. We don't retain anything explicitly created using alloc, but we do have to remember to release them in dealloc:. In awakeFromNib:, we use the user defaults to determine if a value was previously stored for a key. If it was, we set the value in the corresponding NSTextField if the value is a path and still exists. Some reasons the path might not exist are if the iPod isn't mounted, or if you did some rearranging of your files in between times you used the application. If sufficient conditions are met, we enable the Copy It button, since a "legal" copy can now occur. Finally, we configure the progress indicator to use threaded animation. It needs to use threaded animation because the main thread will be busy doing work while we want it to spin, so another thread needs to handle the rendering. Of course, this is all encapsulated and you don't have to handle any of the details.

- (IBAction)copyIt:(id)sender {
        NSString* fileName = [[sourceFile stringValue] 
        [progressIndicator startAnimation:nil];
        if ([[fileName pathExtension] isEqualToString:@"pdf"]) 
                NSLog(@"Trying to open a PDF time");
        else if ([[fileName pathExtension] isEqualToString:@"txt"]) 
                [chunker chunkIt:fileName
                           toDir:[[destDir stringValue] 
                   withSeparator:[separatorValue stringValue]];
        else   {   //this should never happen if users are forced
                   //to select files with the open pane and all values
                   //in typesArray are handled in this event loop
                NSLog(@"Unrecognized file type");
        [progressIndicator stopAnimation:nil];
        [userDefaults setObject:[separatorValue stringValue] 

The method copyIt is fairly straightforward. It passes the source file, destination directory, and separator value specified in the view to chunker, which performs the grunt work. Around the work that takes place, we start and stop the animation of the NSProgressIndicator. If you are parsing relatively small files, you won't see it spin, but for very large files, you should feel like it's time to go and get a haircut.

/* A snippet from the revised version of openFileDialog: */

if ((nil != [userDefaults objectForKey:LAST_SOURCE_FILE]) &&
     [fileManager fileExistsAtPath:
           [userDefaults objectForKey:LAST_SOURCE_FILE]]) 
     [openPanel setDirectory:[[userDefaults stringForKey:LAST_SOURCE_FILE]
else {
        [openPanel setDirectory:[@"~" stringByExpandingTildeInPath]]; 
/* More Code Here*/
[userDefaults setObject:[destDir stringValue] 
[userDefaults setObject:[sourceFile stringValue] 

The method openFileDialog: is almost the same. The differences reflect some sanity checks that are necessary, since we're trying to be helpful to the user and keep track of his or her last actions. You should realize by now that Objective-C code is incredibly readable, and you can figure out what's going on without any explanation. In short, if the last file the user copied still exists, we know that the directory it resides in exists. Therefore, the directory containing that file can be safely opened. Otherwise, we start navigation in the user's home directory as a reasonable alternative. As the method ends, it updates the user default values.

- (void)dealloc {
	[userDefaults synchronize];
	[chunker release];
	[typesArray release];
	[userDefaults release];
	[fileManager release];
	[super dealloc];

The method dealloc is a straight-shooter, as well. The only interesting thing here is the [userDefaults synchronize] line. Periodically the user defaults do synchronize, but we want to ensure this happens in case the application closes in between synchronizations.

Final Thoughts

Come back next time ready to cross the Cocoa-Java bridge and add in PDF support. We'll also discuss the many options available to you if you'd like to further enhance this application. Some enhancements that come to mind include reading files from FTP servers or Internet locations, specifying regular expressions as separator values for books that aren't broken into chapters with keywords, and unmounting the iPod once a copy is complete. If you're not already, you'll soon be a very well-read person.

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