Recently we decided to combine repositories for an OS X and iOS version of an application we’ve been developing. This decision came about after realizing that sharing code was breaking one application and our automated build wasn’t picking up the changes. There are a few solutions for sharing code between apps: static libraries, resource bundles, etc. Most come with all kinds of headaches – the difference between “Clean” and “For reals, Xcode, I want you to actually clean my project.” is always a problem.
We decided to go big and skip all of the static library/resource bundle nonsense. We combined the repositories and created a single Xcode workspace containing both the OS X and iOS targets. We let Cocoapods combine the projects into a single workspace for us, but you can create a new workspace and manually add the projects as siblings if you’d like. We still symlink shared files from one project to the other, but since we’re working in the same repository it’s significantly easier to maintain. Builds for both the OS X and iOS apps kick off whenever there is a change to the repository, ensuring that a change to a shared file is reflected in both apps.
In the PlatformApp sample, you see a project for the OS X, iOS, and Pods target(s). By default, Xcode has a scheme to build each target in each project. This allows us to build each app independently and customize the schemes however we like.
At Elemenet 84, we use TeamCity for our automated builds. Recent versions of TeamCity have a nice Xcode Runner build step built into it, allowing us to get rid of a few custom build scripts. This set up makes it easy to get a build up and running in TeamCity since the Xcode Runner looks for a scheme in your main workspace to run. We can quickly set up a build for each target by simply telling TeamCity where the workspace is and which scheme to use.
Are you using Cocoapods? If not, you should be! In addition to app specific code, we’re also sharing 3rd party libraries. Cocoapods makes it easy to organize, restrict, and share your 3rd party libraries between targets in a single workspace. I’ve created a sample project and Podfile to illustrate just how easy it can be:
# This will tell cocoapods the name of the workspace to generate for us. workspace 'PlatformApp' # We have to define a default platform. We tell cocoapods where to find # the Xcode project and which target to link against. This isn't ideal # and someone should file a feature request against cocoapods. platform :ios, '6.0' xcodeproj 'ios/iOSPlatformApp/iOSPlatformApp.xcodeproj' link_with 'iOSPlatformApp' # We want AFNetworking to be shared between both targets. pod 'AFNetworking' # Here, we define a new target named 'osx'. Cocoapods will generate a # separate static library that contains any dependencies listed here # and in our default group that only links against our OS X target. target :osx do platform :osx, '10.8' link_with 'OSXPlatformApp' xcodeproj 'osx/OSXPlatformApp/OSXPlatformApp.xcodeproj' # Dependencies here will only be linked against the OS X target. pod 'CNSplitView', '~> 0.1.5' # We can even add a target just for testing! target :osxTests, exclusive: true do link_with 'OSXPlatformAppTests' xcodeproj 'osx/OSXPlatformApp/OSXPlatformApp.xcodeproj' pod 'Kiwi' end end # This will generate another library that will link against # the iOS target. We don't want to inherit any default pods # (since our default library will already link against the # iOS target) so we mark it as exclusive. target :ios, exclusive: true do xcodeproj 'ios/iOSPlatformApp/iOSPlatformApp.xcodeproj' link_with 'iOSPlatformApp' pod 'MBProgressHUD', '~> 0.5' end
A well structured Podfile gives you a significant amount of control over which libraries are linked against which targets. It’s not perfect and I have a list of issues I’d like to file against Cocoapods to improve it (e.g. having to define a default platform and project; sharing pods between sub-targets, etc.).
So far this structure has been working out really well for us. We can let Cocoapods do its thing to help us manage 3rd party libriaries; easily share files between apps by just including it in the correct target; and automate builds by simply having TeamCity choose a different scheme based on the configuration.