Open URL From Today Extension

A friend of mine mentioned in passing that he was having trouble getting an obvious, well-documented behavior of his Today extension to work … as documented. According to Apple, a Today extension should use NSExtensionContext when it wants to open its host app, e.g. to reveal a related data item from the Today widget, in the context of the host application.

A widget doesn’t directly tell its containing app to open; instead, it uses the openURL:completionHandler: method of NSExtensionContext to tell the system to open its containing app.

They even cite one of their own apps as an example:

In some cases, it can make sense for a Today widget to request its containing app to open. For example, the Calendar widget in OS X opens Calendar when users click an event.

The idea is you should be able to use a simple line of code like this from within your Today extension. E.g., when a user clicks on a button or other element in the widget, you call:

[[self extensionContext] openURL:[NSURL URLWithString:@"marsedit://"] completionHandler:nil];

Unfortunately this doesn’t work for me, for my friend, or I’m guessing, most, if not all people who try to use it. Maybe it’s only broken on Mac OS X? I was curious, so I stepped into the NSExtensionContext openURL:completionHandler: method, and observed that it essentially tries to pass the request directly to the “extension host proxy”:

0x7fff8bd83b03:  movq   -0x15df0b7a(%rip), %rax   ; NSExtensionContext.__extensionHostProxy

But in my tests, this is always nil. At least, it’s always nil for a Today widget configured out of the box the way Xcode says it should be configured by default. So, when you call -[NSExtensionContext openURL:completionHandler:] from a Today widget on OS X, chances are it will pass the message along to … nil. The symptom here is the URL doesn’t open, your completion handler doesn’t get called. You simply get nothing.

Getting back to the fact that Apple used Calendar as their example in the documentation, I thought I’d use my debugging skills to poke around at whether they are in fact calling the same method they recommend 3rd party developers use. If you caught my Xcode Consolation post a while back, it will come as no surprise that an lldb regex breakpoint works wonders here to how the heck Apple’s extension is actually opening URLs. First, you have to catch the app extension while it’s running. It turns out Today widgets are killed pretty aggressively, so if you haven’t used it very recently, it’s liable to be gone. Click on the Today widget to see e.g. Apple’s Calendar widget, then quickly from the Terminal:

ps -ax | grep .appex

To see all running app extensions. Look for the one of interest, ah there it is:

56052 ??         0:00.29 /Applications/Calendar.app/Contents/PlugIns/com.apple.iCal.CalendarNC.appex/Contents/MacOS/com.apple.iCal.CalendarNC

That’s the process ID, 56052 in this case, at the beginning of the line. Quickly click the Notification Center again to keep the process alive, and then from the Terminal:

lldb -p 56052

If all goes well you’ll attach to Apple’s Calendar app extension, where you can set a regex breakpoint on openURL calls, then resume:

(lldb) break set -r openURL
(lldb) c

Now quickly go back to the Notification Center, and click a calendar item for today. If you don’t have a calendar item for today, whoops, go to Calendar, add one, and start this whole dance over ;) Once you’ve clicked on a calendar item in Notification Center and are attached with lldb, you’ll see the tell-tale evidence:

(lldb) bt
* thread #1: tid = 0xd1a4d, 0x00007fff87f22f1f AppKit`-[NSWorkspace openURL:], queue = 'com.apple.main-thread', stop reason = breakpoint 1.8
  * frame #0: 0x00007fff87f22f1f AppKit`-[NSWorkspace openURL:]
    frame #1: 0x00007fff9213f763 CalendarUI`-[CalUIDayViewGadgetOccurrence mouseDown:] + 221
...

So Apple’s Calendar widget, at least, is not using -[NSExtension openURL:completionHandler:]. It’s using plain-old, dumb -[NSWorkspace openURL:]. And when I change my sample Today extension to use NSWorkspace instead of NSExtensionContext, everything “just works.” I suspect it will for my friend, too.

I’m guessing this is a situation where the functionality of Today extensions might be more fleshed out on iOS than on Mac, and the documentation just hasn’t caught up with reality yet. There are a lot of platform-specific caveats in the documentation, and perhaps one of them should be that, for the time being anyway, you should use NSWorkspace to open URLs from Mac-based Today extensions.