Recently I’ve been working on some iOS 8 app extensions, and I’ve run into a few non-obvious details that might come in handy for anyone else in the same situation. Some of the following relates to bugs still in the system, and so will probably only be relevant for a limited time.
The intended approach is simple: when you tell Xcode to run the extension, Xcode will ask you what host app you want to use. That app launches, you trigger your extension on the test device, and Xcode attaches to the extension so you can debug.
The problem is that breakpoints often fail to actually pause execution, even if Xcode is attached to the process. Not for everyone, but for some of us. This can lead to paleo-style NSLog-based debugging, for lack of any better option. The problem is that Xcode has not been able to locate the extension’s
dSYM file (even though it generates this file). Xcode knows where you want the breakpoints but doesn’t think it has loaded any code that matches them.
After fighting this one for a bit I eventually worked out that breakpoints only work if Xcode’s derived data preference is set to “default”. I’ve used a custom location for a long time with no problems, but with extensions that’s a problem. Switching back to the default got breakpoints working again.
Debugging: Today Extensions
Even with the above, debugging “today” extensions can still be awkward. Unlike other host apps, it doesn’t seem that Xcode can actually kill “Today”. It also seems to have trouble forcing Today to load new versions of extensions. That can be confusing when you’ve changed your code but continue to see unmodified behavior.
There are a few voodoo tricks that sometimes work:
- Closing/reopening the notification center
- Tapping the notification center’s “edit” button, removing your extension, then adding it back.
- Unplugging/reconnecting a test device (quitting/relaunching the simulator might also help).
None are completely reliable though. Therefore: when debugging a Today extension, it’s crucial to put a breakpoint or an
NSLog early in your view controller’s life cycle. I needed
initWithCoder: anyway so I used that. This gives you a visible sign that the new code has actually loaded, eliminating the doubt that inevitably arises.
Status Bar Style
If you’re developing a Share or Action extension, you don’t currently get to choose the style of the phone’s status bar. You may be using a full screen UI where you really want light or dark style, but it’s not your decision. You’ll inherit whatever status bar style the host app uses. It’s probably a good idea to go ahead and design your extension to request the preferred style anyway, so that you’ll be ready when this bug is fixed. In the meantime, you’ll have to live with it.
Today Extension UI Design
The app extension programming guide tells you to
Avoid putting a scroll view inside a widget. It’s difficult for users to scroll within a widget without inadvertently scrolling the Today view.
Later on it notes that
In general, you don’t want to make your widget too tall, because users must then scroll to see all the content.
This sounds like friendly advice to not having your UI suck. In practice these guidelines are enforced by the framework:
Horizontal scroll views don’t appear to be possible. The gesture will be intercepted by the notification center to switch between the “today” and “notification” views.
Vertical scrolling is also impossible because you can’t actually make your view tall enough to require scrolling. The maximum possible height for a today view is the screen size minus the notification center’s own UI elements. Requesting larger values has no effect. The actual value is undocumented, and the only way to discover it is to try to exceed it (for example by requesting the full screen height) and seeing what value you end up with. But that causes UI glitches as you hunt around for the right height to fit as much content as possible without going over the limit. Also, the actual value varies depending on the device screen size and on whether the device is in portrait or landscape orientation (and in case you didn’t realize it, landscape mode is possible for the notification center on all devices that support iOS 8).
Action vs. Share Extensions
When looking over the extension types, you might well wonder whether you should build an “action” extension or a “share” extension. The differences are not completely clear. That’s because in practice, the differences are minimal.
With a share extension, Xcode’s template for the target uses
SLComposeServiceViewController. That class provides a decent basic UI like the one used by Apple’s Twitter or Facebook share buttons. That’s convenient if it’s what you need, but it’s not required. A share extension can inherit directly from
UIViewController for a fully custom design. Conversely, an action extension can use
The only noticeable differences I’ve found are:
- With an action extension you have the option to build an extension with no UI of its own. For example, an extension that translates selected text and returns the translation to the host app.
- Share extensions are presented with a modal-style vertical animation from the bottom of the screen, while action extensions just sort of appear.
- Share extensions appear in the top row of the phone’s activity UI with color icons, while action extensions go in the bottom row with monochrome icons. It’s possible that Apple will enforce rules about what kind of activity can go where, rejecting apps for putting extensions in the wrong row. I haven’t heard of any examples of this happening, though.
If none of those are important to you, the difference is insignificant. I built an action extension, but then later wondered if it should have been a share extension instead. To convert from action to share, all I needed to do was change the share point in the extension’s
com.apple.share-services. I left everything else unmodified, but now it was running as a different extension type.
Enabling modules might not work as expected
In iOS 8 (and other recent versions), enabling modules in Xcode’s build settings means you don’t need to explicitly list all the frameworks you want to use. They’ll be found automatically.
But this isn’t the case with
NotificationCenter.framework, which Today extensions use. If you remove that from the build settings, you won’t get any build warnings or errors. But when you try to load the extension, you’ll get an exception from
libextension.dylib and your extension won’t load. The exception message is not enlightening:
2014-08-16 12:06:53.793 TodayTestExtension[41313:6111763] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** setObjectForKey: object cannot be nil (key: <__NSConcreteUUID 0x7fd729422390> ED3B42F8-66CD-4CB0-BCD5-F3DBA6F34DB5)'
If you’re doing a today extension, just leave that framework in the build settings. It shouldn’t need to be there, but it does.
The App Extension Programming Guide notes that some APIs are unavailable to app extensions. A few examples are listed along with a statement that anything marked with
NS_EXTENSION_UNAVAILABLE in the framework headers is off limits. Fine.
This becomes a little awkward if you’re sharing code between an app and an extension. The
NS_EXTENSION_UNAVAILABLE directive means you can’t write code that does run-time checks, because even referencing the forbidden APIs causes a compile error. One option to deal with this is to refactor shared classes into hierarchies, with a common parent used in both and different subclasses for different build targets. Another is to use the preprocessor via
#ifdef checks. There’s no built-in target conditional– nothing like “
#if TARGET_IOS_EXTENSION”– so if you go this route you’ll have to create your own.
[UIApplication sharedApplication] is off limits. That makes good sense, conceptually. But there’s a lot of useful API on
UIApplication which is suddenly unavailable. For example, no
preferredContentSizeCategory means you can’t look up the user’s preferred font sizing. The lack of the
openURL: method is covered by a method on
NSExtensionContext (well, sometimes, but not in all extension types), but there’s currently no equivalent of
canOpenURL:. If you need to open a URL, you just need to hope that it’ll work.
Frameworks vs. iOS 7
If you are sharing code between an app and an extension, one nice way to do so is to create your own embedded framework to hold the code. On iOS 8 it’ll load dynamically for both cases, so you’re set.
If you still support iOS 7 (or earlier), it’s not so clear cut. Embedded frameworks don’t work there. The App Extension Programming Guide breezily notes that you can use dlopen to deal with this. With that approach you write code to load the framework dynamically at run time rather than rely on iOS loading it for you, if you’ve verified that the code is running on a version of iOS that supports doing so.
But how do you use that code on iOS 7? You don’t. If your shared code is in an embedded framework, there’s no way to execute it on iOS 7. It’s just unavailable.
dlopen approach might be handy if you only need the shared code on iOS 8. If you need it on iOS 7, you’ll need to include it in the app target. And once you do that, you have no need of the framework. You could still use a framework for the app extension, but doing so is not actually useful. You’d be doing the work of creating the framework but not getting any benefit from it. Just include the shared code in both targets.
I’ve filed a bunch of Radars about these, so with luck they’ll be fixed before long. App extensions are still a new thing, so some growing pains are expected.