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 weblog, which mostly covers geeky topics like Macs and Mac Programming.
Useful Tidbits and Egotistical Musings from Dan Wood
Categories: Mac OS X · Cocoa Programming · General · All Categories
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.
permanent link
· Topic/Cocoa
|
We have about 200 help pages for Sandvox, available at docs.karelia.com; also available from our Help menu. We recently added a feature, inspired by some Apple pages, in which there is a link to provide feedback at the bottom of each page. (See this page as an example.)
The link goes to a web-based form that allows the visitor to enter their opinion of the referring page. It's powered on the back-end by a simple PHP script that converts the form submission into an email message to us.
So far we have gotten a handful of good comments, but there are a couple of adjustments we had to implement to make this viable:
I'm glad we added this feature to our documentation. Someday, I might change the link at the bottom of the page to do some "Ajax" magic and reveal a comment form right there on the same page, so that the visitor can see the page that they are commenting on, rather than taking them to a separate form. But that's #87,454 on my list of things to do.
permanent link
· Topic/Cocoa
|
Yesterday, I was interviewed by a couple of folks who are doing a project about the "indie" Mac software community. Many other people I know have also talked to them. I look forward to the report that they are working on.
It was interesting to reflect about the business, and the community that we have. This community is great — we "see" each other on Twitter (where I've been active for a while), iChat, email, and even in person at conferences like WWDC and Macworld Expo. It's something that certainly didn't exist when I "went indie" and started writing Watson and its unpublished predecessor, Museo, back in 2001.
There are dozens of well-known, successful independent Mac developers now. More seem to be taking the plunge each and every day. It's easier to do now because of the community, I think. Of course it also means more competition, and it's harder to get a quality application noticed amid the quickie applications. VersionTracker and MacUpdate are so full now that they are not particularly useful anymore.
There's one thought I expressed yesterday, which I'll reiterate here. Consider it advice for budding Indie software developers. If I could go back in time and give myself some advice, I'd say to plan for having more than one application, and incorporate that into your development and company infrastructure. Even if you are a one-product company for quite a while (as we have been, though I hope that will change soon), it will be much less painful to grow into a multi-application company. I'd even go so far as to work a "dummy" second application into your workflow, just so that you can easily work with a second real application should you ever have one. The "behind the scenes" aspects of the company, such as the structure of your source code repository, your building scripts and utilities, your payment processing methodology, your customer/mailing list databases, your techniques for looking up lost license keys, your online help documents, and so forth, will scale better if you plan for it early on.
permanent link
· Topic/Cocoa
|
Justin "Carpe Aqua" Williams reveals, on his Pinup CalendarBlog , his technique for organizing source code. I thought I'd link to it, since it's useful and very close to what we do. I don't have a strict template I follow (perhaps I should) but his approach is pretty close.
Some additions I'd suggest would be accessors (getters and setters; his "accessors & mutators" could be split up maybe), public methods (general functionality documented in the header file), and private methods or support methods, special functions not exposed to the outside world.
If you have lots of different kinds of delegate methods, you can always split up any of your categories into sub-categories (e.g. TextField delegate, Window Delegate, data source, etc. For a subcategory, you could forgo #pragma mark - to make the subcategory stand out less in the popup.
If you have a class that is really huge, it's actually helpful to split it up into multiple files. One ".h" file, declaring multiple categories, with the implementation of each category defined in separate files. For example, our document window controller subclass is so big, it merits nine files!
A final hint: define a macro "mark" to insert the two pragma-mark lines, using Completion Dictionary.
permanent link
· Topic/Cocoa
|
Terrence has written a couple of really cool posts about work he's done that greatly speed up & simplify build processes in Cocoa & XCode....
permanent link
· Topic/Cocoa
|
I've been hearing complaints about how big PNGs are when saved by Sandvox, but I finally got around to testing out this issue. Check out these results.
| Source | Dimensions | File Size |
|---|---|---|
| JPEG original before reducing & sharpening | 1200 x 1600 | 912.1 KB |
| -[NSBitmapImageRep representationUsingType: NSJPEGFileType] (quality 0.7) | 480x640 | 101.6 KB |
| -[NSBitmapImageRep representationUsingType: NSPNGFileType] | 480x640 | 376.1 KB |
| Above PNG then processed by PNGCrusher | 480x640 | 303.7 KB |
Notice that the PNG file when saved from Cocoa are almost four times the size of the JPEG. Of course, the quality (0.7) is going to impact the JPEG size, but that's still quite a hit!
When I processed the output file through PNGCrusher, It was able to squeeze a good chunk of wasted space out of the file. This means that the compression algorithm supplied by Cocoa isn't very optimal. (It would be nice to have a parameter to pass in to determine how much to optimize, the tradeoff being speed of course.)
I wanted to get a sense of how Preview.app would do with this. I saved the scaled-down image from my program as lossless TIFF, then opened and saved as PNG from preview. The results were almost the same, just a few bytes in difference. It's good to know that at least Cocoa's API isn't worse than what Preview uses. As with the API, it might be nice to have some control over how optimized the compression should be!
I think I'm going to file two bugs to Apple. One requesting API access to better PNG optimization; another requesting Preview to control this optimization.
permanent link
· Topic/Cocoa
|
I spent the last weekend at iPhoneDevCamp, meeting some nice folks and working on our TeleMoose project, an iPhone web-based application that makes it easy to browse and shop Amazon.com.
It's been a fun side project to work on. We're still putting most of our Karelia resources into Sandvox of course, but the idea of this project (which came to us during the WWDC week) made so much sense, considering that I had done something very similar for Watson many years ago, then did it again for the port to Java that never saw the light of day other than some demos; we have also used Amazon Web Services for Sandvox in the inspector for one of our many pagelets.
There has been a huge vacuum of information so far, and it's been a process of trial and error in many cases to figure out the best way to get an iphone-specific web application built. Sven has been working with me on some CSS issues and has documented some annoying limitations in iPhone support. At iPhoneDevCamp, I learned a lot of new techniques, and I discovered a lot as well. I've documented my iPhone discoveries in a quickie website I put together, TeleMoose at iPhoneDevCamp. Due to a lack of WiFi connections there, we wound us using the slow EDGE network for access from our iPhones; this gave me an opportunity to explore bandwidth-saving techniques; I gave a presentation on this topic; the slides (a PDF from keynote that ironically take up a lot of bandwidth!) can be found on that site, along with a few other notes.