Inside StYNCies
Pages: 1, 2
Hide the Application Menu
Although our project isn't complete, now is a good time for a gut check, so tell Xcode to Build and Go. You should not have any compiler errors, but definitely will have some warnings because there's a lot of method bodies not defined yet. You should be able to run your application, see the symbol up on your status bar and successfully quit the application using the symbol's drop-down menu.
You'll get a runtime message that the syncAndEject: action couldn't be connected, but we already know this. The point is to get our bearings and take care of any typos that have occurred up 'til now. But wait a minute--there's still a full menu when we run the app. One other thing we can knock out right now is backgrounding the app to remove that menu bar. Double-click on Info.plist in the Resources folder of Xcode's left pane to open up the plist editor. Expand Root and click on any of the items in the dictionary. Click on New Sibling up top and add the key "LSUIElement" (Launch Services User Interface Element) with a value of 1. If you don't have Xcode set up to edit plists with the Property List Editor, you can change this to your default in Xcode's File Types preferences, open the Property List Editor in your /Developer/Applications/Utilities folder, or just add in this plain text to the dictionary by viewing it as plain text.
<key>LSUIElement</key>
<string>1</string>
Save the changes, and choose Build and Go in Xcode. Your menu problems have just disappeared--literally.
Finishing off AppController
Let's finish off AppController. Once that's done, all that remains for this installment is to define the IPodUtils class. Check out the method bodies below and copy them into your AppController.m file.
- (void)mountNotification:(NSNotification*)notification
{
if ([podUtil deviceIsiPodAtPath:
[[notification userInfo]
objectForKey:@"NSDevicePath"]])
{
[self synciPodsAndEject:NO];
}
}
- (void)synciPodsAndEject:(BOOL)eject
{
NSArray* iPods = [podUtil connectediPods];
NSEnumerator *enumerator = [iPods objectEnumerator];
NSString *podPath;
while (podPath = [enumerator nextObject])
{
NSString *stickiesPodPath;
NSString *stickiesPrefix;
BOOL toNotes;
if ([podUtil deviceHasNotesFolderAtPath:podPath])
{
stickiesPodPath =
[podPath stringByAppendingPathComponent:
@"/Notes/StickiesSync"];
stickiesPrefix =
[NSString stringWithString:@""];
toNotes = YES;
}
else
{
stickiesPodPath =
[podPath stringByAppendingPathComponent:
@"/Contacts/StickiesSync"];
stickiesPrefix =
[NSString stringWithString:@"!"];
toNotes = NO;
}
//remove any existing "StickiesSync" folder on iPod
[fileManager removeFileAtPath:stickiesPodPath
handler:nil];
//create new "StickiesSync" folder on iPod
[fileManager createDirectoryAtPath:stickiesPodPath
attributes:nil];
//copy in the notes to "StickiesSync"
[self copyStickiesToiPodAtPath:stickiesPodPath
withPrefix:stickiesPrefix
toNotesFolder:(BOOL)toNotes];
if (eject)
{
[[NSWorkspace sharedWorkspace]
unmountAndEjectDeviceAtPath:podPath];
}
}
}
- (IBAction)syncAndEject:(id)sender
{
[self synciPodsAndEject:YES];
}
- (void)copyStickiesToiPodAtPath:(NSString*)path
withPrefix:(NSString*)prefix
toNotesFolder:(BOOL)toNotes
{
/* TODO next time */
NSLog(@"Syncing StickiesDatabase to iPod next time...");
}
You've noticed that syncAndEject: is simply a wrapper that calls synciPodsAndEject: with an argument of YES. Our finished application will sync the iPod each time it's connected to the computer, as well as any time the user chooses to sync it from the menu bar. Passing the buck to synciPodsAndEject: allows the reuse of identical code that's also needed in the method mountNotification, as will be seen. The method mountNotification retrieves the path of the recently mounted device and passes it to our iPod utility class (still undefined at the moment) to determine if the device is an iPod, and conditionally calls synciPodsAndEject:.
In synciPodsAndEject: we find most of the action. In short, the iPod utility class returns to our controller an array of paths that correspond to mounted iPods, and is able to determine if the device is older than a 3G iPod. If it is older than 3G, the notes are copied into its Contacts folder using the vCard format the iPod expects to read via a call to copyStickiesToiPodAtPath:.
A common file prefix is added to each note copied in order to group the notes together. The Contacts folder orders items alphabetically, so if notes don't share a common prefix, they get scattered throughout your contacts, which is not very convenient. If the iPod is 3G or newer, the notes can be copied into the Notes folder as plain text and ordered alphabetically. Each sync is accomplished by removing any existing notes on the iPod and then copying in the most recent ones from the Stickies application. The implementation details of reverse engineering the database Stickies uses is left until next time.
Creating iPod Utilities
Before this installment finishes, let's fill in the IPodUtils class. There's no rocket science involved here; it's just some grunt work that's convenient to encapsulate into a easier-to-use class. The interface for class IPodUtils is like so:
@interface IPodUtils : NSObject {}
- (id)init;
- (void)dealloc;
- (NSArray*)connectediPods;
- (BOOL)deviceIsiPodAtPath:(NSString*)path;
- (BOOL)deviceHasNotesFolderAtPath:(NSString*)path;
@end
You should be able to see how these methods fit into everything based on their method names. The only "clever" thing we do here is take advantage of some things we know about all iPods: 1) they all have an iPod_Control directory and 2) 3G and new iPods have a Notes folder, while older models do not. These two details allow us to determine if a device in question is indeed an iPod and how we should go about storing text notes into it. Here are the implementation details:
#import "IPodUtils.h"
@implementation IPodUtils
- (id)init
{
return [super init];
}
- (void)dealloc
{
[super dealloc];
}
- (NSArray*)connectediPods
{
NSArray* allVolumes =
[[NSWorkspace sharedWorkspace] mountedRemovableMedia];
NSMutableArray* iPodVolumes = [[NSMutableArray alloc] init];
int i;
for (i=0;i<[allVolumes count];i++)
{
NSString* iPodControlPath =
[[allVolumes objectAtIndex:i]
stringByAppendingPathComponent:@"iPod_Control"];
if ([[NSFileManager defaultManager]
fileExistsAtPath:iPodControlPath])
{
[iPodVolumes addObject:
[allVolumes objectAtIndex:i]];
}
}
[iPodVolumes autorelease];
return iPodVolumes;
}
- (BOOL)deviceIsiPodAtPath:(NSString*) path
{
return [[NSFileManager defaultManager] fileExistsAtPath:
[path stringByAppendingPathComponent:@"iPod_Control"]];
}
- (BOOL)deviceHasNotesFolderAtPath:(NSString*)path
{
return [[NSFileManager defaultManager] fileExistsAtPath:
[path stringByAppendingPathComponent:@"Notes"]];
}
@end
If you remember to add a #import "IPodUtils.h" directive to the top of your AppController.h file, you should be able to compile with no errors. Your user interface is now complete. You should run your app and verify that it is working. If you have an iPod connected and choose Sync Stickies and Eject, you should see the output in the Run Log console, and your iPod should eject. Once that's done, redock it to see that your app detects it and issues the statement again to the console.
At this point, all that's left is to pull data out of the StickiesDatabase file located in ~/Library and send it over to the iPod. But wait, Apple hasn't published any API for Stickies. Reverse engineering, anyone?
You'll see that a little intuition and clever guessing allows us to develop our own API without significant pains. If you want to get ahead, take a peek at the documentation on NSUnarchiver before next time. If you like where this is going, you can also download StYNCies, a more sophisticated version of the app we're building.
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 MacDevCenter.com.
You must be logged in to the O'Reilly Network to post a talkback.
Showing messages 1 through 13 of 13.
-
nib ?
2005-03-21 19:39:55 JeffR [Reply | View]
-
nib ?
2005-03-21 19:48:15 Matthew Russell |
[Reply | View]
Ok, given that you're relatively new to this, let's start off with the most basic question: Have you looked up in the grey menubar on the top of your screen for a little black parallelogram? It won't be there unless you're running the program, but you should see it if it's there beside your clock, volume, or whatever else is up there?
Have you gone through the steps to make the application faceless by modifing the plist yet?
Let me know and we'll take it from there.
M.
-
leaks?
2005-03-14 08:41:27 hieper [Reply | View]
You seem to be leaking instances of IPodUtils and NSFileManager (and NSStatusItem) in your AppController. (any reason why you don't use [NSFileManager defaultManager]?
Where are mRTFDData etc. declared, or will you introduce them later?
Also, there is no need to declare -(id)init et. al. in the interface, since they are in NSObject.
patrick -
leaks?
2005-03-14 09:17:30 Matthew Russell |
[Reply | View]
Patrick,
Thanks for pointing those things out. In the process of taking a full project and breaking up into two pieces for each of the articles, I neglected to remove the mRTFDData references. Those should just be commented out for the moment; they'll be introduced in the next piece as part of a class that's used to read/write from the StickiesDatabase file.
There's no particular reason why I didn't use [NSFileManager defaultManager]. That way is probably more preferred as you seem to imply, however, so there's no reason not to go with it.
As for the dealloc issues involving memory, the dealloc I have here on my machine didn't turn out to be the same one that was published. Amazingly, what is in the article is the first half of AppController's dealloc method joined with the last half of a dealloc method from a class not introduced yet. Not quire sure how that happened; must have been a cut/paste error. Try this one instead:
/*dealloc for AppController */
- (void)dealloc
{
NSNotificationCenter* nc;
nc = [NSNotificationCenter defaultCenter];
[nc removeObserver:self];
[statusItem release];
[prefsController dealloc];
[podUtil dealloc];
[userDefaults dealloc];
[fileManager dealloc];
[super dealloc];
}
As for calling or not calling init in the interface--I know they're inherited from NSObject. It's just my preference to make the interface and implementations match, and something I think of as a good practice. I personally find this to be a good indicator to use for when something's being overridden. Feel free to differ if you have different preferences.
Thanks again for helping to clear those things up.
M. -
leaks?
2005-03-17 01:47:45 hieper [Reply | View]
[prefsController dealloc] ??? -
leaks?
2005-03-17 07:04:35 Matthew Russell |
[Reply | View]
As you mentioned, breaking up is hard to do. Given the context of what's going on, you don't have a prefsController. Axe that line. -
leaks?
2005-03-14 13:10:58 hieper [Reply | View]
I'm still not happy with this version of dealloc, the other one was better, really. I guess Neil Sedaka was right: 'breaking up is hard to do" ;-)
It would be nice if you could correct the main article text with a correct version.
About NSFileManager, I don't now why, but somehow the shared defaultManager is stuck in my mind as being 'preferred'. Maybe it is because the documentation says: You invoke all NSFileManager instance methods with this object as the receiver, whichs sound a bit like 'you must...'. It doesn't give a real reason though.
greetings,
patrick
-
Help with the build process.
2005-03-13 16:16:43 thenewguy [Reply | View]
Sorry but this problem is unrelated to the project you posted. I was at one of the first columns that showed how to make a text editor but I thought noone would respond there so I posted the problem here.
Every time I tried to build it gave me the same 2 lines. I just named the project game when I started.
-
Help with the build process.
2005-03-13 16:27:10 Matthew Russell |
[Reply | View]
That explains a lot! ;)
To get in touch with the author of the article you're working through, you have to post your comments there. The author will most likely still get your message, even if it's an old article. If that doesn't work out, you can always try the Cocoa mailing lists.
An excellent reference for your Cocoa programming endeavors is Aaron Hillegass's book, Cocoa Programming for Mac OS X, which I highly recommend. I find it to be superb, and it has helped me out a lot.
M.
-
Help with the build process.
2005-03-12 19:57:27 thenewguy [Reply | View]
My Project Builder gives me this when I press the build button:
Missing file or directory: Game
Missing file or directory: 1_Prefix.h
Please help! -
Help with the build process.
2005-03-12 21:12:31 Matthew Russell |
[Reply | View]
One other thing is to clean your target (from the build menu) and try to rebuild just in case some cache corruption is the issue. -
Help with the build process.
2005-03-12 20:38:52 Matthew Russell |
[Reply | View]
The reference to "Game" appears to be a reference to something that's totally unrelated to this project. Did you start with a fresh new Cocoa application in XCode v1.5? The reference to the Prefix header being missing seems to indicate that this might not be the case, because that's something that normally gets taken care of automatically.
At any rate, I need a bit more info than what you gave me.
Is this your first Cocoa project ever? (need to know so I can provide better guidance for you specifically...not sure since your name is "thenewguy")
Is this your first attempt at building this project? At what specific point in the article are you trying to build?
Do you have any build settings from any other projects that might be throwing things off for any reason? The reference to "Game" is totally unexpected. Have you done any recent projects involving "Game" or know of any reason why you'd have any references to any such thing?
Give me some more info and I'll try to get you going.
M.






I followed the directions, but I think I have screwed something up in the IB portion. When I compile and run the app I can plug the ipod in and see the message that syning will happen in the future, but I don't see the 2 item menu anywhere, did I miss a hookup somewhere that would cause this?
JeffR