Framework Images and WatchOS 2

I hate API hacks and workarounds, but WatchOS 2 needs first class support of framework assets.

Despite getting a major API update with WatchOS 2, I'm actually kinda irked with the rather closed access we have to the Apple Watch. My biggest gripe is the complete lack of access to view management and dynamic UI element creation. This is very likely a result of the need for speed and efficiency on an embedded platform with severe power constraints, but it doesn't make things any happier.

Nevertheless, WatchOS 2 also brings a set of interesting challenges as it relates to frameworks. Because WatchOS 2 is a completely separate platform during the compilation process, sharing a common framework gets a little brittle. Apple's guidance, as outlined in their WatchOS Transition Guide, sets out these steps for sharing code:

  1. Open the project editor pane of Xcode. (The pane is normally closed.)
  2. Control-click the target to display a context menu with a Duplicate command.
  3. Change the name of the target so that you can identify it easily later.
  4. In Build settings, change the following values:
    1. Change the Supported Platforms setting to watchOS.
    2. Change the Base SDK setting to the latest watchOS.
    3. Change the Product Name setting so that it matches the name of your iOS framework. You want both frameworks to be built with the same name.
  5. Add the framework to your WatchKit extension’s list of linked frameworks.

The biggest problem with this approach is that as code or resources are added to a framework, the developer must remember to add them to both targets. I find this to be a bit cumbersome and introduces a potential point of failure.

One of the big advantages of a framework over, say a static library, is that we can include resources like a Core Data model, images, etc. Images are an especially important opportunity for reuse and are quite expensive from a compiled binary size, so any opportunity for reducing duplicate assets can be quite useful. These images can be handily loaded with a class method on UIImage, imageNamed:inBundle:compatibleWithTraitCollection:. Unfortunately, with the introduction of iOS 9, there is a conditional preprocessor check that excludes its inclusion on WatchOS 2. The particular check is:

#if __has_include(<UIKit/UITraitCollection.h>)
+ (nullable UIImage *)imageNamed:(NSString *)name inBundle:(nullable NSBundle *)bundle compatibleWithTraitCollection:(nullable UITraitCollection *)traitCollection NS_AVAILABLE_IOS(8_0);
#endif

If your framework uses this API for loading its resources and is used in a WatchOS 2 app, you'll get a compilation error. The way I have found a way around it is actually add it back in:

#if TARGET_OS_WATCH
@interface UIImage (WatchKit)

+ (nullable UIImage *)imageNamed:(NSString *)name 
                        inBundle:(nullable NSBundle *)bundle
   compatibleWithTraitCollection:(nullable id)traitCollection;

@end
#endif

You'll notice that I replaced the reference to UITraitCollection and swapped it for an id. As of WatchOS beta 5, this works and the image is loaded. I hate API hacks and workarounds, but WatchOS 2 really needs to support image loading through bundles without any lower level hijinks.

It is unfortunate that Apple has made using frameworks a somewhat hostile endeavor on WatchOS 2, but I am hopeful that with a few radar filings and persistence we will see better support in the future.

Posted on Aug 31
Written by Wayne Hartman