Dan Wood is co-owner of Karelia Software, creating programs for the Macintosh computer. He is the father of two kids, lives in the Bay Area of California USA, and prefers bicycles to cars. This site is his older weblog, which mostly covers geeky topics like Macs and Mac Programming. Go visit the current blog here.
Useful Tidbits and Egotistical Musings from Dan Wood
Categories: Business · Mac OS X · Cocoa Programming · General · All Categories
permanent link
· Topic/Cocoa
Update: Rainer Brockerhoff told me that you can actually put an 'RTF ' resource in the resource file, rather than TEXT/styl, so it turns out that this is not the easiest solution.
One thing that I try to accomplish each January is to update our copyright statements in all the right places. The only straggler that remained was the Software License Agreement that is embedded in our disk images.
Unfortunately, the text for this is not stored in any way that makes it easy to update things. The rich text is stored as an old-fashioned Circa 1984 resource fork, as TEXT and styl resources. (Apple's "Software License Agreements for UDIFs" SDK [Download DMG] has all the gory details.)
It's barely possible to even edit a resource file nowadays. There is the open-source ResKnife but it doesn't seem to have TEXT/styl support. There's ResFool, but at least as of this writing the website isn't even loading. There are of course options like ResEdit and Resorcerer that one can run under Classic mode from older versions of Mac OS X, but that's not particularly convenient!
I really was hoping I could just store our SLA text as rich text (like .rtf files) and have the conversion to the resources handled for me automagically!
No wonder many developers use tools like DropDMG and DMGCanvas. The trouble is, we have all of our build/package/upload process as a shell script, run from a build phase on Xcode. So I really needed a scriptable tool to produce the correct resources.
I did some digging, and I found a nice hint (at the bottom of this page) for converting rich text to TEXT/styl resources by using NSPasteboard.
So I whipped up some code that goes something like this.
int main (int argc, const char * argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; if (2 != argc) { NSLog(@"Usage: %@ srcpath\n", [[NSString stringWithUTF8String:argv[0]] lastPathComponent]); return -1; } NSString *sourcePath = [NSString stringWithUTF8String:argv[1]]; NSAttributedString *str = [[NSAttributedString alloc] initWithPath:sourcePath documentAttributes:nil]; NSData *data = [str RTFFromRange:NSMakeRange(0, [str length]) documentAttributes:nil]; NSPasteboard *pb = [NSPasteboard generalPasteboard]; [pb declareTypes:[NSArray arrayWithObject:NSRTFPboardType] owner:nil]; [pb setData:data forType:NSRTFPboardType]; NSData *textData = [pb dataForType:@"CorePasteboardFlavorType 0x54455854"]; // TEXT NSData *styleData = [pb dataForType:@"CorePasteboardFlavorType 0x7374796C"]; // styl
All I needed to do was to write the NSData to a ".r" file. I found a bit of code via Google, from an app called JPEGBatcherX. It looks like the author was doing something similar. I adapted his dump_rsrc method from this source file which was directly indexed by Google: rtf2r.m. (I don't want to reproduce the code here because it's not clear what the copyright on the code is.)
So the above function should be able to end like this:
dump_rsrc("TEXT", textData); dump_rsrc("styl", styleData); [pool drain]; return 0; }
However, I hit a snag. When running on my Intel Mac, this is an invalid 'styl' resource. I had to dig up an old copy of Resorcerer and run it under Classic on my old G5 to figure out why.
A valid 'styl' resource starts something like this:
0060 0000 0000 000F 000C 0400 0100 000C
This corresponds to: 0x0060 style runs, 0x00000000 first offset, 0x000F line height, 0x000C font ascent, 0x0400 font family, 0x0100 char style, 0x000c pixel size, and so forth.
However, the style data I was getting from the pasteboard was more like this:
6000 0000 0000 0f00 0C00 0004 0001 0C00
It sure looks like a little-endian vs. big-endian issue. This perplexed me, considering I was not manipulating numbers; I was just asking the pasteboard for a bit of data that I ought to be able to write into the 'styl' resource.
Fortunately Daniel Jalkut mentioned some methods that were designed to flip resource data around. He later explained "Apple provided flippers for many common resource types, but left us to our own devices for some of the less common/modern ones."
I guess this make a small amount of sense. Rather than Apple rewriting the resource manager code to deal with endian issues, we just flip the data afterwards. Still, it's a bit tricky if you don't expect that!
So all I needed was to copy the bytes to a writeable block and call the appropriate routine to flip them around. Note my #ifdef in the code so that in case the code is big-endian, no real flipping will happen.
int len = [styleData length]; char *bytes = malloc(len); [styleData getBytes:bytes length:len]; OSStatus status = CoreEndianFlipData ( kCoreEndianResourceManagerDomain, //OSType dataDomain, 'styl', //OSType dataType, 0, //SInt16 id, bytes, //void *data, len, //ByteCount dataLen, #ifdef __BIG_ENDIAN__ true #else false //Boolean currentlyNative #endif ); NSData *newStyleData = [[[NSData alloc] initWithBytesNoCopy:bytes length:len freeWhenDone:YES] autorelease];
That does the trick! Now all I have to do is to concatenate the TEXT/styl resources produced with this tool to with the other (un-changing) resources I'm storing in a common ".r" file and build them into my disk image, using the 'Rez' tool.
permanent link
· Topic/Cocoa
At the job that I had before starting Karelia Software, around the turn of the century, our software engineering department had a fairly intense set of coding guidelines. Not just the formatting rules that people had to follow (e.g. no K&R braces, indentation rules, etc.) but also many guidelines that were about programming defensively. The idea was that you had to write code that was less likely to have errors in it, and was less likely to have errors introduced later on.
Although the guidelines were originally written for C++ (for old-school Mac development), a lot of the rules transferred easily to Java (as did the many of the developers!), and then, with me, when I went "indie," to Objective-C.
I don't have a copy of that document, alas, but I did manage to internalize many of those rules, so that it became habit for me (for the most part), and I've been able to add on to some of those rules to be specific to Objective-C/Cocoa programming.
A few days ago, I found out about the LLVM/Clang Static Analyzer — "Clang" for short — from a number of developers on Twitter (where I am "danwood", BTW, and a lot more active there than on this blog these days). I ran our codebase (Sandvox, along with the iMedia Browser and other bits of code) through the analyzer, and what I found was very interesting: Most of the bugs that it found, if we had better applied the guidelines we've been trying to follow, would not have been there. I consider this proof of the utility of these kinds of guidelines, which I'll list later on here.
permanent link
· Topic/Cocoa
I make my living off the evening news
Just give me something-something I can use
People love it when you lose,
They love dirty laundry
— Don Henley, "Dirty Laundry"
So we are close to releasing Sandvox 1.5 over in Karelia-land. But there are a few exceptions and crashes that people are getting, and we're out of ideas on how to diagnose them. (Of course, they don't happen to us!)
So Karl suggested posting the backtraces and seeing if any of our astute readers (of the Developer persuasion) had some ideas. Can those of you not daunted by this prospect take a look and see if anything comes to mind, any thoughts that you might have to help track down the issues at hand? I have some good juicy backtraces with symbols, the trick is figuring out why these crashes or exceptions happened. (For the crashes I am just including the backtraces but I could upload the full crash reports if people want...)
I'd love to hear your comments in the comments section, or you can drop me a line: dwood at the venerable domain of karelia, in the kingdom of Dot Com.
permanent link
· Topic/Cocoa
I've been spending the last few days, on and off, dealing with localizations of the new Sandvox version we are trying to get ready. Each time we have a new version coming out, I have to go through a lot of tedium and hassle to successfully get Sandvox updated and fully functional in several languages. It really could be a lot easier than it is now.
The current version of Sandvox has eight localizations. Besides English, it's in Danish, French, German, Italian, Japanese, Traditional Chinese (Taiwan) and Simplified Chinese (China). The localizations are done by some wonderful, amazing volunteers. (We may have to drop a couple of supported localizations for our forthcoming 1.5 version; we've lost a couple of our volunteers unfortunately.) The volunteers all use iLocalize, which is a great (but by no means the only) localization tool out there; if they don't have the program we get them a license for them to use it.
The ideal workflow goes something like this: When we are getting near a release, we send a private build of Sandvox out to these localizers. They each open the application bundle with iLocalize, and it figures out what strings need to be translated, keeping track of the strings that have already been translated. When they are done, they export the resources (.strings files, .nib files, and a few .html files) and email them back to us. Using a shell script, I copy the resources into a new directory structure that parallels our subversion repositories (since the topology of an application package is not the same as our source code). Another script copies the files into the corresponding places in our subversion repository, careful not to stomp on the .svn directories.
In an ideal world, that would be pretty easy. The problem is, a release is a moving target. Even though we declared we were "frozen" for strings about a month ago, little things crop up -- a bugfix may change a string or require a new error message. Or last-minute changes, like the debut of MobileMe, mean that we have to go through another round of localization. And frustratingly, we find that even after we have accepted a localization, merged it into our source repository, and built a new version, that new version isn't quite fully localized. Somehow, some strings got missed.
The worst problem in all this is nib files. Once we have localizations in play, a change to a nib (adding or repositioning a label, hooking up a forgotten outlet or action or binding, etc.) has to be propagated to all the localizations, or there will be a bug in your program when it's running under other languages. This means that the programmer has to go through and make the same change n times, or rely on the translator to know that they have to rebuild the translated nib from the English (source) version. (I'll assume that English is your source language for the rest of this article.)
It's very difficult to make sure that all our languages are really working when maintaining multiple nib files is so fragile.
".strings" files, though they are a lot simpler and just text files, are not without their problems as well. If you fix a typo in your English text, you have to make sure to fix that source string in all of your translated .strings files, or your source will not be found and show up as English in your otherwise translated program. You are not likely to know that there is a problem unless you can test every message your program will ever encounter in all its translations. (Peter Hosey's Localization Helper is a utility that can help with this a bit, though.)
The problems of dealing with translations are so great that several developers have abandoned localized nibs completely — CocoaTech has done this for Path Finder, as has Delicious Monster for Delicious Library. I'm sure there are others. The alternate approach is to have a single nib, putting in translated strings on-the-fly, and possibly resizing the views programatically to make the elements fit. (Languages such as French and German take up quite a bit more space than English!)
On-the-fly localization is a decent idea — we're considering it for our next major version of Sandvox to alleviate some of these hassles — but it's not without its drawbacks. You really have to prepare your nibs for the "worst case" scenario of elements fitting. There are some scenarios in Sandvox that I'm not sure how we could work around: for instance, some labels are so long in their translation that we have to split them into two or three lines to fit. This affects the horizontal positioning of everything else. If we left room for the eventual translation, then the English version would look strange. When you are aligning elements with each other (for instance, right-justifying several labels to the left of some checkboxes), you would need to move all the items that were aligned. Or, you just leave a lot of blank space in the English version — a feat that sounds like a good idea but is hard to accomplish in small elements such as inspector windows.
Solutions?
It seems that Apple needs to take Internationalization ("i18n" for those who don't want to type a twenty-letter word) a lot more seriously and integrate this directly into Interface Builder and Xcode.
Xcode is slightly aware of localized files, and Interface Builder and Xcode are slightly aware of each other, so I don't see why there couldn't be more direct i18n support built in.
Imagine if your localized files were just always synced with their original counterparts. If I adjusted a string in my Objective C file, the English .strings file could be automatically updated, and so would the translated versions (with a warning marking to indicate that somebody will need to check the translations.) If I deleted an English string, the corresponding localizations would be deleted too, since they wouldn't be needed. If I added a new string, the translated files would have entries for those new strings, marked as needing to be translated.
If I modified a nib, I'd only have to deal with the English nib. I could add a connection or binding, fix the spelling of an action's method name, etc. and not have to worry about the translated nibs getting out of sync. If I added or changed the contents of a textual label, then the translated nib files could be marked as needing to be translated or checked. If I deleted something, they would be gone in the translated nib. If I moved or resized something, the translated nibs might indicate a warning if the corresponding string is conflicting with something.
(I'm always finding that it's very difficult to see when a string has not been given enough room in a translation, and we're forever getting complaints that buttons in German are truncated. Not surprising, considering I don't speak the language and thus I can't tell at a glance if the text has been truncated! There needs to be big bold warnings that text is getting truncated in your nibs!)
Perhaps what is needed is an overhaul of the way that nibs are stored. Yes, i know that the .xib format in Leopard is new, but couldn't nib files be split into connections (one per nib, period), strings (one per localization), and layout (one per localization if needed), so that it's impossible to get the kinds of inconsistencies that we developers are always fighting?
Maybe we need more ways to lay out items in a nib so that automatic repositioning is easier, so you can avoid a lot of the headaches of multiple layouts to check. In the simple cases where a button is the only element on a "line" and you make the text wider, that's a no-brainer. But when you start dealing with groups of items side by side that need to align with each other, I don't think that there is enough information in the struts-and-springs model we use now to automatically "stretch" things appropriately to fit different languages automatically.
What do you think? Are there any tricks to localization that I haven't covered here?
If you agree that Apple needs to make localization easier and more integrated, please file a new bug and say "me too" to radar ID 6078830 ...
permanent link
· Topic/Cocoa
The reason that the Mac developer community is thriving so much is the exchange of information among developers to help fill in the gaps in documentation and understanding of the details. Most Mac applications have been developed because of the open access to information about development and the ability to share that information. I can understand the iPhone SDK being under Non-Disclosure Agreement (NDA) before the release of 2.0, but to continue the silence is making things very bad. It's preventing conferences, classes, books, and tutorials from happening. Is there *anything* good about the NDA continuing? I think not.
I am ready to dive into iPhone development — we have not quite yet take the plunge — but if an NDA continues, I may just sit this one out.
Here is an interesting idea: Gather petitions showing the strong desire for the iPhone SDK to be open so that developers can discuss things. This isn't just for developers to sign! If you are an iPhone user, you are only going to benefit in the quality of the iPhone applications available if Apple lifts the NDA. If they don't, your applications are not going to be as good.
permanent link
· Topic/Cocoa
Yesterday, Michiel van Meeteren released Indie Fever, his thesis about the "Indie" Mac developer culture. It's over 100 pages, and a bit technical — his field's terminology, not ours, though! But it was certainly an interesting read, and I recommend it for current or future indie Mac developers.
One thing that struck me was the notion that we members of the development community are competitors: "Despite these collaborations they still regard each other as competitors although all sorts of unspoken rules apply to the kind of competition that is allowed within the community."
Yes, there are some competitors in this community, meaning that their products overlap in functionality enough to attract potentially the same customer base. But most of the people I interact with in the developer community are not competitors at all, unless you really stretch the definition by saying that we are competing for the attention and hard-earned dollars of the Mac users out there. Miciel compares the nature of the community to "the close-knit craft communities of Northern Italy or the diamond merchants of Antwerp." I don't know if that's quite accurate, if they are all selling the same things.
To choose a fun metaphor, we're the vendors at an electronic farmer's market. I might be selling peaches, but the vendors around me are selling honey, vegetables, flowers, and jars of curry. I'm not going to have any poroblems with the guy who's selling zucchini in the booth next door; in fact we're probably going to be buddies and help each other out.
And if there are enough people mulling around the market, I'm probably not going to mind the other guy selling peaches across the aisle (unless he's, say, giving them away for free; I'd need to make sure my peaches were better than his).
You see, there are something like 20 million users of current (Tiger or Leopard) Macs out there. Sure, it's less than the numbers of Windows PCs out there, but who cares. This is a large potential customer base. We indies, in order to make a living, really only need to make customers out of a very small fraction of the Mac users out there. An indie developer needs only a few thousand customers to make a living; we're talking only about one one-thousandth of the current Macs out there.
Since there are plenty of potential users of our software to go around, I like the idea of each other helping each other out. We've had a Good Karma section on our website and in our periodic email alert blasts, where we highlight some of our favorite indie apps. This is one of our ways of being part of the developer community.
I may have some more thoughts about the developer community and the business of being an Indie in subsequent posts. Stay tuned.
permanent link
· Topic/Cocoa
In Sandvox and iMedia Browser, we use a number of utility windows that are singletons; there will be only one of any of them open. Examples include the registration window, email list signup dialog, problem reporter window, and the release notes window.
I had noticed a lot of redundant code in each of these controllers' classes dealing with maintaining the single instance of that window, which we would store in a static variable and access with a lazy accessor, not unlike the technique here.
When something starts to get repetitive, it's time to find a better way. So I created a class KSSingletonWindowController which manages the singleton instances. Instead of storing each instance of a window controller in its own static variable, it maintains a static reference to an NSMutableDictionary holding the window controllers, keyed by an identifier (generally the subclass name).
The class contains four class methods:
@interface KSSingletonWindowController : NSWindowController + (id)sharedController; + (id)sharedControllerWithoutLoading; + (id)sharedControllerNamed:(NSString *)registrationName; + (id)sharedControllerWithoutLoadingNamed:(NSString *)registrationName; @end
To access a window controller, you only need to invoke sharedController on your subclass of KSSingletonWindowController. The object will be created if it is not yet registered, and then that single object will be returned to you. If you want to get the instance but only if it has already been loaded (useful, for instance, in clean-up code or a response to a method to close a window; there's no point creating it if it hasn't already been created) you can use sharedControllerWithoutLoading.
If you need to have more than instance of a class, you would need to register each one with a different name. For example, you might have one instance of RTFDWindowController which you use to show release notes, and another one for showing credits. You would use the methods sharedControllerNamed: and sharedControllerWithoutLoadingNamed:, passing in arbitrary strings to identify each unique instance.
Here is the implementation. Each method builds upon the previous ones.
static NSMutableDictionary *sControllerRegistry = nil; @implementation KSSingletonWindowController
sharedControllerWithoutLoadingNamed: lazily instantiates the controller registry and looks up the entry for the given key.
+ (id)sharedControllerWithoutLoadingNamed:(NSString *)registrationName { if (!sControllerRegistry) { sControllerRegistry = [[NSMutableDictionary alloc] init]; } KSSingletonWindowController *result = [sControllerRegistry objectForKey:registrationName]; return result; }
sharedControllerNamed: looks up the named item. It creates and stores the object if it wasn't allocated yet.
+ (id)sharedControllerNamed:(NSString *)registrationName { KSSingletonWindowController *result = [self sharedControllerWithoutLoadingNamed:registrationName]; if (!result) { result = [[[self alloc] init] autorelease]; [sControllerRegistry setObject:result forKey:registrationName]; } return result; }
sharedController and sharedControllerWithoutLoading call their "named" counterparts using the name of the class as the key. (Obviously, you would invoke +[YourClass sharedController], not +[KSSingletonWindowController sharedController] for this to work.)
+ (id)sharedController; { NSString *className = NSStringFromClass(self); return [self sharedControllerNamed:className]; } + (id)sharedControllerWithoutLoading; { NSString *className = NSStringFromClass(self); return [self sharedControllerWithoutLoadingNamed:className]; }
That should do it! Please feel free to use and adapt this for your projects. Enjoy!
permanent link
· Topic/Cocoa
Following the example of Panic and others, we've put some code into our applicatons that, upon first launch, will ask people if they want to join our email announcement list.
One thing I don't like is when computers are dumb. And what I thought would seem dumb would be the case where you are already on our email list, and you hear about our new app, so you download it from the link we provide, and upon launching said app, it asks you if you want to join the email list. Well, duh!
So I mulled this over in the back of my head for a long time. Should we have a special version of our application that skips this question? Wow that would be a lot of hassle and be potentially confusing. Or would there be some way we could use AppleScript to set the user defaults for our application so that it wouldn't ask? That would probably be pretty invasive and require the user running a script in ScriptEditor. Or download a helper application that sets the stage in advance, then launches the real application? Geez, that's not going to be obvious. There must be a better way....
Somewhere along the way, it occurred to me: Cookies. Yeah, that's it. We can access the cookie store from Cocoa; it's the same cookie store used by Safari or other Webkit-based browsers. True, some Mac users are using Firefox or Camino, but this might solve it for a good chunk of users.
So what I did was to store a cookie associated with our website onn the page that a person goes to when they have confirmed that they want to sign up for our mailing list. I also set that same cookie when somebody clicks on the download link in our email blast; the link takes them to a page that sets the cookie and starts downloading the desired file.
Our application, upon launching, first checks the user default that will be set to prevent being asked more than once. If that has not been set yet, it checks the cookie store. If the cookie indicating that the they are on our email list is present, then they will be spared the redundant question. Only if the default and the cookie are absent will they be bothered.
The cool thing is that this cookie works across multiple applications. The cookie check is built into iMedia Browser 1.1, which we just released this morning, but we will be building it into Sandvox as well. Once that cookie is set, they won't be asked again from any of our applications. Well, at least until the cookie expires — but by then they will have stored the user default to prevent being asked.
Clearly, this is not a fool-proof technique. The cookie store might not have been written to disk by the time somebody launches the application. The user might have cleared out their cookies, or isn't using a Webkit-based browser. But it's an interesting, light-weight mechanism for "communicating" between Browser and Desktop.
Naturally the next thing to think about is "What else could you accomplish with this technique?" You could set cookies on your company's website that your desktop application might be able to take advantage of, e.g. your most recent login ID (but not password of course), recently visited pages, and so forth. The "communication" could go the other way as well; a desktop application might set some cookies that help give the corresponding website some hints for web content.
This is a very "weak" form of communication. If you want to literally control your Desktop application from a website, you could put in hyperlinks to custom URLs that your application can respond to by implementing this kind of code. (For instance, this link, if you already have been running iMedia Browser, will open up the application's feedback window with some pre-populated fields; we've set this up to help people follow up on tech support issues.) And of course from your desktop application you can open up arbitrary URLs in your browser using NSWorkspace operations.
But when you want non-obtrusive passing of interesting data or settings between desktop and browser, this may be a useful technique.
permanent link
· Topic/Cocoa
Here is a snippet of code you can put into any subclass of NSValueTransformer that will cause it to automatically register itself when the class is loaded. This is useful for almost all value transformers, except perhaps those that you need to be given a parameter in the initialization process, such as these cool transformer classes.
This code will just cause the transformer to be registered with the name of the class itself. So if you class was, for instance, CondenseTransformer then you would specify "CondenseTransformer" in your nib file.
+ (void)load { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSValueTransformer *theTransformer = [[[self alloc] init] autorelease]; [NSValueTransformer setValueTransformer:theTransformer forName:NSStringFromClass([self class])]; [pool release]; }
permanent link
· Topic/Cocoa
Users of Leopard may have noticed that if you have to force-quit an application, you are prompted to submit a report to Apple, not unlike the crash reporting mechanism we are all used to.
As a developer, it occurred to me that this information would be very useful for me (and third-party developers in general) as well.
I did some investigating, and found that these reports are stored in a subfolder of Library/Logs/HangReporter/. (☃, it turns out, did some analysis of this folder a while back. Incidentally, it looks like the problem of the UNKNOWN folders has been fixed.)
Unfortunately, this directory, and the subdirectories for each application's reports, are root-only. I'm supposing that Apple put in this protection so that applications couldn't tell what other applications were doing, but the problem is that there is no way for the third-party application to reasonably look to determine if there is a new spin report that needs submitting. I don't think a user is going to tolerate being asked for an admin password every time they launch an application, just so the application can check to see if there are any new reports!
It would be much more useful if application-specific spin reports could be put into a user's home directory, and made readable to that user. Then, third-party developers could cobble together a mechanism for reporting a hang, just like many of us do for crash reports.
Saving spin reports is a really cool new feature in Leopard, but if there is no way to help the third-party developer, it's just a potential feature.
Filed as rdar://5879393.
permanent link
· Topic/Cocoa
If you are programming to target Leopard, a lot of common graphics can be found using -[NSImage imageNamed:]. Not every generic graphic can be found there; there are a lot of images in /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/.
It's certainly worth exploring the contents of this folder and possibly making use of the images. But since these images are not documented as an API, you will need to be sensitive to whether or not these images are still part of upcoming — or legacy — versions of Mac OS.
Since Sandvox is targetting either Tiger or Leopard, we have to be careful not to make use of a graphic that is in one big cat and not the other.
For our convenience, and for yours, I created a "diff" showing the contents of the Tiger vs. Leopard contents of the graphic files in that folder, after the break.
permanent link
· Topic/Cocoa
We at Karelia have been recently going through a bunch of our source code, pulling out the general-purpose stuff apart from the Sandvox-specific stuff. Some of this is worth sharing.
Fellow Karelian Mike Abdullah has written a nice class that extends Core Data in a nice way. It's called KSExtensibleManagedObject. Check it out.