1/21/2014: Update #1: forgot a step! New step 8 at end. Update #2: updated step 5.
As of iOS 8, Apple is allowing dynamic frameworks in iOS applications submitted to the App Store.
Hey, it’s easy! Just make a new Cocoa Touch Framework target in your project. And you’re done!
(Hint: you’re not done.)
There’s actually a bunch of mistakes you (read: I) can make while converting your Objective-C code to use iOS 8 frameworks, so I’m writing it all down while I’m thinking about it.
1. Move all your files over to the framework.
It’s easy to miss a couple. And it may look like everything’s working if you also forget to remove them from your app (see 2), except you’ll get linker errors when you later try to compile your app extension.
.m files go in Compile Sources, .h files go in Headers (see 3), .xib and .storyboard files go in Copy Bundle Resources. This is all the same as for apps, except for Headers, which I’ll get to in a moment.
If you do it through the Target Membership pane of the utility area to the right of the Xcode window, adding them to the right Build Phase is done for you automatically when you click the checkbox for the framework target.
2. Remove all your files from other targets.
The Target Membership pane is the easiest way to do this, as you can both add and remove files from targets there. And you can multiple-select files in the Project Navigator on the left-hand side of the Xcode window, and do it all at once.
What I would expect, if you compile and link a .m file for both your app and your framework, is some sort of duplicate symbol error.
What you get, in Xcode 6 beta 2 at least, is “Definition of ‘DuplicateSymbol’ must be imported from module ‘MyFramework.DuplicateSymbol’ before it is required.” Which is confusing, to say the least.
3. Set the right visibility for your headers.
Like with OS X frameworks, with iOS frameworks, if you add a header to the framework target, it is be default put in the “Project” visibility group. This, like “Private”, means it will not be available to apps or extensions that link against the framework. If you want your header to be visible to them, you must change its visibility to “Public”.
For the curious: (a) “Project” headers aren’t shipped with the framework binary. (b) “Private” headers are shipped, but in a separate directory, called “PrivateHeaders”, which, as far as I know, is only visible if you’re Apple.
4. Rename your imports.
Here’s where things get fun.
First of all, keep in mind that, as far as linking is concerned, the name of your framework is the Product Name, not the name of your target. Normally, these are the same, since the
PRODUCT_NAME build setting is set to
$(TARGET_NAME). But it’s something to keep an eye on if you get build errors.
With that out of the way: if you moved any header files to your framework, it’s a good idea (if not required) that you change their imports.
Local headers in your app are imported like this:
But framework headers, at least before all this module business, were referred to like this:
Now, nobody imports the individual headers, of, say, a framework like Foundation. They use the umbrella header, like this:
The same is true for your new framework. Xcode makes a so-called “umbrella” header for you with the same name as the framework, and then complains if you don’t import all your public headers there.
The neat thing is, using the new module import syntax, you don’t even need to refer to the umbrella header at all, you can just leave it off completely and the compiler knows what you mean:
@import Foundation; @import MyFramework;
Though you still can refer to individual public headers within framework the if you really want:
@import Foundation.NSArray; @import MyFramework.MyHeader;
One place you can’t use the new syntax, however, is the umbrella header itself! The compiler will complain that “Import of module ‘MyFramework’ appears within same top-level module ‘MyFramework'” and “No submodule named ‘MyHeader’ in module ‘MyFramework'”.
So for now, sadly, you need to use the old format when adding all the necessary public header inclusions to your framework’s umbrella header. (You can see this in Apple’s umbrella headers as well.)
5. Don’t add module names to your storyboard and xib files for Objective-C.
If you look at the Identity Inspector pane in Xcode’s utility area with, say, the File’s Owner entry selected in your xib file, in Xcode 6 you’ll see a Custom Class pane with a second field for module name. (Actually, sometimes you’ll see two Custom Class panes, but never mind that now.)
Updated: (Thanks, Jordan!) This is needed for Swift classes, which I won’t be describing in this post.
Adding the module name for Objective-C classes leads to runtime errors: “Unknown class _TtC11MyFramework18MyClass in Interface Builder file.” When that happens, the instance of the class, instead of being of the right type, is of type UICustomObject.
So don’t do it!
6. Runtime IB errors indicate missing .m files.
Note, if you leave out a .m file from your framework that’s only instantiated from a xib or storyboard, the only error you’ll get (apart from things not working) is a log message, “Unknown class MyClass in Interface Builder file.”
You get this, instead of a linker error, presumably because there’s no compile-time linking going on at all; instead, the runtime instantiates the object entirely via an arbitrary class name string.
Actually, scratch that. I see this problem with classes whose absence should cause framework linker errors. So I don’t know what’s going on, exactly. But if you see that error, go over step 2 again.
7. Fix the dylib warning in your app extension.
If I make a brand-new iOS app extension, then make a brand-new framework, then link the framework against the app extension, without any other changes, I got a linker warning:
Linking against dylib not safe for use in application extensions: /Path/To/Framework/MyFramework.framework/MyFramework
Now, from WWDC, I know that there are certain UIKit APIs, marked with a
NS_EXTENSION_UNAVAILABLE_IOS macro, that are not allowed in app extensions.
This is a more general warning, however, and you fix it by going to the General tab of your framework in the Project Editor, disclosing the Deployment Info section, and clicking the “Allow app extension API only” checkbox.
After that, poof, no warning, and if you try to use any APIs that are marked with the macro
NS_EXTENSION_UNAVAILABLE_IOS, you’ll get a build error that specifically mentions the call site. Everybody wins!
8. (NEW!) Fix your bundle references.
In iOS, all your resources are located in your app’s main bundle. So for methods where you have to specify a bundle to use for resource loading, such as
-[UIViewController initWithNibName:bundle:] you can safely pass
nil, meaning use the app’s main bundle.
If your framework code is running in your app, however, that won’t work. The resource isn’t in the app’s main bundle, it’s in the framework’s main bundle. Specifying the app’s main bundle via nil means your resource won’t be found.
There’s an easy way to fix this, however:
[NSBundle bundleForClass:[MyClass class]]. As long as
MyClass was compiled into your framework’s binary, not your app binary, the framework’s main bundle will be returned, without your having to hardcode its name or path. This is how OS X developers have been doing forever, and it works just peachy.