Migrating from alias handles to bookmark data
OS X 10.6 Snow Leopard introduced a wealth of new APIs on NSURL. In particular, a set of methods for dealing with bookmark data.
A great many developers, Karelia included, have a need to keep references to files that might be moved to a new location by the user. Prior to 10.6, the best way to accomplish this was by using the old Carbon Alias Manager to create alias records, and then store the record’s data.
Snowy supersedes those APIs with native Cocoa methods on NSURL. They’re pretty straightforward and match up pretty closely to the old alias record concepts:
- The bookmarkData… methods take a URL or alias file and create bookmark data from it. You can then persist that data as your app sees fit
- +URLByResolvingBookmarkData:… performs the reverse, locating the best match for the file on disk (there’s an equivalent init method too)
Various options enums can be passed in to adjust quite how these methods operate. As well as the API reference, Apple have a guide document.
Do you have existing alias record data that needs bringing into the new world? (This includes the popular NDAlias and BDAlias wrappers). You might imagine “bookmark data” is just Cocoa’s terminology for a wrapper around the old APIs, but that’s not the case.
Bookmarks use their own custom format (I assume Apple has good reason for this) that’s incompatible with the alias manager. Conversion is one-way and you have to drop down to CoreFoundation: CFURLCreateBookmarkDataFromAliasRecord.
Now is the time to look at adopting these APIs, since OS X 10.7.3 introduces an extension to the bookmark format: security-scoped bookmarks. NSURL has new options and methods to accommodate this. There’s a good chance your app will want to adopt these for when/if sandboxing rules come into effect for the app store.
Why I worry that demanding permission before accessing contact data is too crude
Apple have said that they will soon be updating iOS to check for explicit permission before an app can have access to contact data.
Consider an app like Sandvox. Yes, it doesn’t ship on iOS, and Apple haven’t announced if the Mac will be receiving the same treatment, but there are plenty of legitimate reasons like ours to use the address book API on iOS. So:
When you create a new document/site in Sandvox, we look up your “Me” card in order to pre-populate the site’s title and footer. It’s a nice touch that makes the new site instantly feel more yours. Under the scheme Apple is proposing, this access would first require you to grant Sandvox permission to access your address book. It seems to me that at this point there’s then a variety of possible reactions:
- Annoyance/fear: “Why I am I being bugged for this? What is the app going to do with my info?” — made even worse if the info turns out to be irrelevant to the site!
- “I have no idea what this is for; I’ll just say OK like I do to all alerts” — pretty common (if bad) behaviour that we’d be serving to reinforce
- “I have no idea what this is for; I don’t want to grant access” — we’ve now bugged the user to no gain
- “Hopefully this is for something smart and innocuous, I’ll say yes”
Consider me sceptical, but I think the last of these is going to be fairly rare! It seems at this point the tiny gain of this Sandvox nicety would be outweighed by all the possible ways to annoy/confuse/upset customers.
Of course I suppose Apple could add some new API that lets us tell the user why access is requested. That seems a potential trojan nightmare though; the alert could be a complete lie! In theory the app store reviewers should be able to catch such behaviour — perhaps this API would only be available to app store apps?
When sandboxing starts being widely adopted, we have the beginnings of a solution to this for app store apps. Without the address book entitlement, apps are unable to use the entire API. It becomes a lot harder to sneak through an app that access the data in an inconspicuous way. But that won’t stop apps with a legitimate use for the data from abusing it, such as uploading to a server to keep.
Perhaps there could be two or more tiers of entitlement? This Sandvox feature only needs access to the “Me” card and a few fields. Apps like Path could have a different entitlement for access to hashed data only.
Who knows really? We’ll just have to wait and see what the future brings.
Living with Twitter 4
The tweet detail view displays full conversation history, including any replies to the message. I’ve been wanting this for a long time; it’s great! I expect other clients have done this sort of thing before, but I never got round to looking at them. Either way, hopefully there’s an API so third parties don’t have to grope around piecing together the equivalent. Also, the conversation is presented in the correct chronological order — brilliant!
Switching accounts is way more fiddly. I appreciate most people have only the one account, but as someone with a company account on the side, it’s annoyingly awkward.
The Tweetie tab bar used to confuse me regularly as to where exactly I was in the app; going back to the standard one is preferable.
Direct messages are tucked away under the Me tab. I regularly fail to notice the blue glow indicating a new one. Fortunately, for all of its bugginess, a large chunk of what I used DMs for before has shifted over to iMessage. The Discover tab periodically marks itself as “unread”. I don’t give a fig for its contents, so force myself to ignore the blue glow, compounding the problem of not noticing DMs.
Search seems to work better. Placing it under the Discover tab feels a little odd to me, in spite it actually being a pretty logical choice! I miss the ability to search your timeline, even though I used it rarely. The improved — to my perception at least — general search is making up for it so far.
The use of a “grouped” style for the main table views, rather than “plain” seems an odd waste of space. There’s less content on-screen now, which for a service people scan through quickly is irritating.
Improving NSDocument's sheet handling
NSDocument has handy little method: -windowForSheet. Its main purpose is to figure out, when the doc has multiple windows associated with it, which is most appropriate for displaying a sheet.
I’ve found it does have one downside: what if your document is already displaying a sheet? Under such circumstances, the document’s window is still returned, leaving the system no option but to futilely beep as it fails to present a second a sheet.
This is of particular importance with Lion’s new document saving model. If a document is locked and you try to edit it, a sheet comes down informing you that the document is locked, and giving a chance to recover by unlocking, duplicating, or cancelling (performs an undo op). All very well until it happens the edit being made is in a sheet over your document window. Cocoa tries to present that error sheet and fails, leaving the edit intact. This could be just about excusable were it not for the fact that then attempting to undo the edit confuses the system further, bringing that very same warning sheet up, despite the fact you are trying to go back to the unedited state!
Well fear not, here’s a simple tweak you can implement in your document subclass to adjust the default behaviour:
There is of course one potential problem: If you have code that repeatedly tries to show a sheet, regardless of what’s onscreen already, you can end up with quite a stack of sheets, whereas before it was less obtrusive with the secondary sheets just causing a beep. Hopefully your code doesn’t do anything quite so daft as this, but I have found that a failed autosave can trigger this behaviour, should you ignore the initial error and keep switching between apps.
A convenience for NSAlert construction
Often times, you want to construct an alert so that it contains more than a simple localized string. As ever, that can be a bit tedious building up the formatted string yourself, so here's a quick convenience category you can add to your own projects:
Clearing a document's undo history while still marking it as edited
When publishing, Sandvox keeps a record as part of the document as to what it has uploaded to the server. Upon the next publish, it can easily consult this record to see what changes have occurred, and upload only them.
But here's the trick: once you've published something to the internet there's no way to "undo" it; you can only delete it or overwrite it. Similarly in Sandvox, once published there is no sense in undoing changes to the publishing record; to do so would make no change to the server, and would leave Sandvox with the wrong information come the next publish.
Since the publishing record is part of the document, and we don't alter documents on disk until explicitly asked to do so (i.e. the act of saving the doc), the sensible solution is to clear out the undo manager's history upon publishing. Which is where we finally hit upon the point of this post: how to do just that!
“How hard can that be Mike?”, you ask. Well, there are a few caveats. So without further ado, here's a good general purpose snippet:
Restoring multi-windowed documents on Lion
Lion introduces a new feature called window restoration, whereby when relaunching an app, it comes back just as you left it. Plenty of people across the internets seem upset by it, but I happen to love it.
Happily, document-based apps — without any extra effort — get restoration of document windows for free! Quitting and relaunching will reopen any previously open documents, almost as if by magic, even if you haven't yet adopted autosave-in-place behaviour. This seems to anger some internet people even more.
But what if your document happens to have multiple windows, some of them optional? We have exactly that in Sandvox: there's a main document window, but optionally, extra windows associated with the doc for Code Injection. We were trying to tighten up Sandvox's window restoration to include such windows, and frankly it's a mystery to fathom! The aforementioned restoration-haters make it even harder, by occupying most of the search results. Fortunately we battled through and found the answer:
This method is not in the documentation (rdar://problem/10437425), but is in the headers, and it's the key to the whole thing:
@interface NSDocument (NSRestorableState)
- (void)restoreDocumentWindowWithIdentifier:(NSString *)identifier state:(NSCoder *)state completionHandler:(void (^)(NSWindow *, NSError *))completionHandler NS_AVAILABLE_MAC(10_7);
@end
Find it in NSWindowRestoration.h for full details.
For most apps it already does the right thing, as witnessed by the way doc windows automatically restore. To handle optional, secondary windows takes a little more work though. Here's a rough version of the solution we arrived at in Sandvox:
The important thing to note is that Cocoa's standard behaviour only knows how to create document windows by calling -makeWindowControllers. Anything more advanced, such as here, and you have to override to create it yourself.
The final piece of the puzzle then, is how to give your secondary window a unique identifier. We tried to do this in Interface Builder, but found that went ignored for some reason. Something as simple as this works pretty well:
(Out of the box, all document windows get given the same identifier by the system)
Targeting Leopard, using blocks when available
A scenario cropped up recently where I want to pass a block into an API when running on Snow Leopard and greater, but fall back to the old block-less API before that. Sounds simple enough, but launch the app on Leopard and what’s this?
Symbol not found: __NSConcreteStackBlock
Expected in: /usr/lib/libSystem.B.dylib
Uh-oh. What to do? I tracked down this Stack Overflow answer for the same problem with iOS3, and applied it to Sandvox. It works! In Xcode simply add this to your target’s “Other Linker Flags”:
-weak_library /usr/lib/libSystem.B.dylib
Resetting a managed object context doesn't always do what you might expect
As noted in my previous post, NSPersistentDocument implements reversion by calling -reset on the managed object context. I was playing with this for Sandvox, and ran into somewhat of a problem with it, which may be an unusual edge case; I'm not sure!
We're using the binary store, and also implementing old-style autosave — whereby a copy of the document is periodically saved in case the computer dies or Sandvox crashes.
If you attempt to revert the document after it's been autosaved, the file on disk does indeed go back to the correct reversion (in fact it's not touched at all). But the context only reverts back as far as the autosaved state. What's happening here?
It seems that, at least for atomic stores, -[NSManagedObjectContext reset] only resets the state of the context. It doesn't reread the data from disk, instead relying on its own existing, cached copy of the data.
Fortunately I was able to figure out a workaround; rather than reset the context, do something like this:
The trick, basically, is to replace the existing store with a new one, thus triggering a refresh of Core Data's cache.
A major downside of doing this mind, is that the context will be temporarily unusable while there is no store attached, so make sure all controllers etc. depending on the context are able to handle this, or torn down (as NSPersistentDocument does)
Reverting NSPersistentDocuments
NSPersistentDocument is documented to implement some extra work on top of what NSDocument provides. The docs say:
Revert resets the document’s managed object context. Objects are subsequently loaded from the persistent store on demand, as with opening a new document.
That sounds a little painful! -reset is a pretty hardcore thing to do; how will my UI cope? Perhaps the description for -revertToContentsOfURL:ofType:error: will help?
Overridden to clean up the managed object context and controllers during a revert.
Ooh, cleaning up controllers sounds pretty magical! But, er, how? What? Are we talking about NSArrayController etc.? Who knows!
Finally, the Lion release notes have this to say:
• Overriding -revertToContentsOfURL:ofType:error:
When you enable autosaving in place you also enable version preserving. (Unless you also override +preservesVersions to turn version preserving off.) With autosaving in place enabled it's more important than ever to invoke super when you override -revertToContentsOfURL:ofType:error:, because it's NSDocument's default implementation of that method that updates the document's state to reflect what happens during reverting to an old version. If you don't, NSDocument might present the user with alerts about the document having been changed by another application when that is not the case.
Does NSPersistentDocument call super, or provide its own, equivalent (one hopes!) implementation?
Well, fortunately for you, I have done some digging — albeit in the pub, so bear that in mind.
How does NSPersistentDocument revert the managed object context?
It does indeed call -reset
How does the UI cope?
Before resetting the context, all window controllers are torn down. After the reset, -makeWindowControllers is called to recreate whichever windows the document pleases.
I think this is a reasonable compromise to make; while Cocoa is theoretically capable of keeping the same window intact and updating it to match the reverted model, that's going to be quite a pain to implement. Plus under Lion, with animated window opening and closing, the effect is quite instructive/pleasing.
Is -[NSDocument revertToContentsOfURL:ofType:error:] called?
Yes indeed, which in turn calls -readFromURL:ofType:error:, so make sure if you've overridden that, you can handle reading having already happened.
How does all this come into play with Versions on Lion?
When using the Versions browser, if the user chooses to revert back to an older copy of the document, Cocoa will call -revertToContentsOfURL:ofType:error: on your existing document. It won't take the easy route of throwing away the the document and starting again; in fact I can't find a suitable API for going down this route if you'd prefer.

