Lessons from converting complex submodules to Cocoapods
I have recently converted two fairly large iOS libraries (written in a mixture of Objective-C and Swift) from a plain git submodule to a set of dynamic frameworks that can be used as Cocoapods. The process was quite tedious, so here’s a random collection of hurdles that had to be crossed.
1. The conversion
- You need a
{MyModule}.h
file, which can be a dummy or, better, imports the Objective-C header files that you want to have public. - Objective-C code that’s referencing Swift within the Cocoapod needs to use
#import <MyModule/MyModule-Swift.h>
. - If you’re using custom macros that live in your prefix header, consider removing them as it’s not recommended to use those in Cocoapods, but, if you want to keep them, move them to a new file and then specify that as the
prefix_header_file
in your podspec. - If you’re splitting up your submodule into multiple Cocoapods (not subspecs) then use
@import {MyModule};
to reference it from Objective-C, andimport {MyModule}
to reference it from Swift. - Make sure to include your resources, especially
xib
files as Xcode likes to put them into theClasses
folder, but they need to get added as a resource. You might end up with something like this:
spec.source_files = [
...,
"Classes/{Directory}/**/*.{h,m,swift}",
]
spec.resources = [
...,
"Classes/{Directory}/**/*.xib",
]
- Speaking of resources, if you’re using CoreData, make sure you’re adding the CoreData model file to the Cocoapod. This is getting a bit tricky with versioned models and will look like this:
spec.resources = [
...,
"{ModelName}.xcdatamodeld",
"{ModelName}.xcdatamodeld/*."
]
spec.preserve_paths = '{ModelName}.xcdatamodeld'
spec.frameworks = 'CoreData'
You might of course be hitting more problems when doing the conversion of your submodule to a framework and/or Cocoapod. These links might prove useful:
- Apple’s intro to Frameworks
- Apple’s guide to using Swift and Objective-C in the same Framework
- A StackOverflow question regarding mixing Swift and Objective-C in the same pod
2. The dreaded “Cannot find interface declaration for ‘X’” errors
This might have two causes:
Firstly, the simple one, Swift classes and methods that are used in Objective-C need to be declared public.
Secondly, the trickier one, it happens if you’re mixing Objective-C and Swift, you’re extending a custom Objective-C class with a Swift extension, and you’re trying to use a Cocoapod subspec. This is creating a circular dependency between the {MyModule}.h
file and the {MyModule}-Swift.h
file. You cannot do this; use a dedicated Cocoapod and not a subspec.
3. When it finally compiles
That means, you’re mostly there, but an important thing to keep in mind is that anything in a framework will not be part of your main bundle. This means you might also need to do the following both within your library, but also when using the library:
- Don’t use
[NSBundle mainBundle]
when accessing resources from the library, but rather[NSBundle bundleForClass:[{myClass} class]]
. - Don’t use
imageNamed
, butimageNamed inBundle
. - Same for
nibWithNibName
- Same goes for
NSLocalizedString
, which you need to change toNSLocalizedStringFromTableInBundle
and the relevant Swift version1.
4. The benefits
In the end, it was well worth it, mostly due to the fact hat each library (or component thereof) can now easily be encapsulated as a dynamic framework. For motivation, here’s a list of benefits that we reaped:
- Most obviously, the libraries can now much easier get imported into new projects; dependencies are also added automatically.
- It’s fairly straightforward to also support Carthage. The main thing to do here is to add a project2 and a framework target.
- Integration with Travis CI and other continuous integration tools is much easier (especially after adding Carthage support)
- It forced us to identify and resolve a bunch of dependencies, including circular ones, between the components that make for bad design.
- Resources, including translations, are now properly encapsulated in each library. This has resolved a critical crash in the Xcode’s Xliff translation toolchain for us, which was ultimately our motivation to go through all of this.
- No more manually adding files and resources to each project that is using the library whenever new files or resources get added.
- Theoretically, you only need to compile the framework once (rather than over-and-over as Xcode would do when the source code in your project) and then redistribute it; however, this can currently crash the debugger.
-
Xcode’s find and replace using regexes will come in handy. E.g., find
NSLocalizedString\("(.*)", tableName: "{MyModule}", comment:(.*)\)
and replace it withNSLocalizedString\("$1", tableName: "{MyModule}}", bundle: {MyBundleHelper}}.bundle(), comment:$2\)
. ↩ -
Or a workspace, if your library is depending on other Cocoapods. ↩