Scalable Swift

Swift may be super-fast at runtime, but during compilation… well, it can be slow. Linkedin revealed in a blog post that they gave all their developers Mac Pros to get their compile times under control. I’ve experienced a project that takes 10mins each time you want to launch the simulator with 40KLOC. I had high hopes that Swift 3 would have an optimised compiler and solve this, however I found my compile times took around 30% longer after I migrated from 2 to 3. Also, after a codebase hits a certain size, the incremental compilation appears to give up, and any changes result in a full re-compile. And finally, code completion becomes unusably slow (or simply fails) after a certain codebase size too. Needless to say, these problems are disastrous to your productivity, so what can we do to solve them? Hopefully I can help you below.

Frameworks

My main recommendation is to split your app up into frameworks. Now I’m certainly not recommending creating your own cocoapods or carthage libraries or git submodules - I’ve done that before, it’s way too much effort. However, with Xcode’s nested projects, this solution can work with minimal ongoing maintenance.

The main architectural issue here is that you need to enforce careful separation of concerns, so that your code will split neatly into mostly-independent frameworks. Once you’ve done this, Xcode will only compile just the frameworks that have changed and your compile times + indexing + code completion should improve.

I recommend splitting your app up into frameworks like so:

  • Helpers: These are anything that needs to be used across the board, like currency formatting or styling or common views. This framework is the ‘tip of the pyramid’ when it comes to dependencies: it is to be used by all your other frameworks, but it cannot depend on anything else.
  • Services+Models: You may want to separate your model objects from your API services, but in my experience it’s not worth bothering. These may rely on helpers.
  • Navigation: You’ll probably have a custom home container controller and a bunch of helpers for various complicated navigation - I’d put them in here. This may also depend on helpers.
  • Many individual feature frameworks: This is where the magic happens: All your apps independent features, categorised into individual frameworks.
  • And: whatever else makes sense for your project.

How?

How do we make these frameworks? Here’s the process for adding to a workspace. If you’re using a project only, it may be slightly different:

  • Open your existing workspace.
  • File > New > Project
  • Select ‘cocoa touch framework’ because static libraries are objc-only (I assume this is because the ABI isn’t stable yet).
  • For product name, use eg ‘Helpers’
  • Team: none
  • Org name: MyCompany
  • Org identifier: whatever your normal bundle id is, with ‘.frameworks’ appended, eg: com.mycompany.myapp.frameworks
  • Click ‘next’ and it’ll ask where to place it with a save dialog.
  • Under ‘add to’, select your workspace. Under ‘group’, select the folder you want it to be nested under - folders have a yellow icon (eg skip the workspace and project levels).
  • De-select git - you want this subproject to be simply part of your normal git repo.
  • It should automatically choose the based on the group you chose, but double check and click ‘create’.
  • In Xcode, you should now see ‘Helpers.xcodeproj’ nested under the group you chose.
  • Configure the deployment target at the project level to the same as your main project.
  • If you have extra configurations in the main project, add them to the subproject.
  • If you have per-configuration build settings for your main project, you may want to add these to your subproject.
  • Click on the main project, then for your target(s) do the following in the General tab:
  • Scroll to ‘Embedded binaries’ and add the framework MyApp/Foo.xcodeproj/Products/Foo.framework
  • Check it automatically got added to ‘linked frameworks’. You may need to add it if its not there.
  • You can make frameworks rely on other frameworks, just don’t make circular references. Eg the main app can use Helpers and Services, and Services can also use Helpers. But Helpers cannot then use Services.
  • When creating/moving code into your framework, you’ll have to mark some classes and functions as ‘public’ to provide an interface to the main project.
  • You can use open instead of public if things have to be subclassable. I recommend using public where you can because it’s like java’s final, eg: no subclassing, which gives the compiler room for optimisations.
  • Anywhere you wish to use code from your framework, you’ll need to have import Foo at the top of your swift file in the main/consumer project.
  • Share the scheme for the new project: Product > Scheme > Manage, then select ‘shared’ next to the project.
  • Celebrate!

Gotchas

There really aren’t too many downsides to this approach, but some things to watch out for:

Xcode will recompile the frameworks if you change them automatically before recompiling the main module. But if it can’t compile a framework, it might try compiling the main project against the last compiled version of the framework, and give a bunch of weird errors. So sometimes you should select the subproject’s scheme and try building that first.

Sometimes have to turn off SWIFT_WHOLE_MODULE_OPTIMIZATION to get useful errors if builds are hanging.

Sometimes when you have compile errors, it’s worth looking in the Cmd+8 tab to see the build output, in case it’s an earlier error in one of the frameworks that is snowballing to further things.

To load images that are stored in the main project’s xcassets from the subproject, you’ll have to do this: UIImage(named: "X", in: Bundle(for: type(of: self)), compatibleWith: nil). Alternatively you can put assets in a bundle in the subproject which is tidy.

Update:

Apparently if you have an app with too many frameworks, you can increase your app’s startup time. For instance, it is common to see 5s startup time on iOS9 with ~20 frameworks. As of iOS9.3 onwards, this problem is largely mitigated, and it is more common to see a 0.3s delay with a dozen frameworks. Apple recommends only having 6 or less frameworks in your app. I haven’t done any tests, and I cannot say for sure if these techniques would be any slower than having one monstrously large app module. But this is something to keep in mind. You can read more here: useyourloaf.com/blog/slow-app-startup-times

One more thing

As of writing (Xcode 8.1) you can often get a significant compile speedup by adding the following user-defined build setting, it may be worth a try for you: SWIFT_WHOLE_MODULE_OPTIMIZATION = YES

Thanks for reading, I hope this helps!

Thanks for reading! And if you want to get in touch, I'd love to hear from you: chris.hulbert at gmail.

Chris Hulbert

(Comp Sci, Hons - UTS)

Software Developer (Freelancer / Contractor) in Australia.

I have worked at places such as Google, Cochlear, Assembly Payments, News Corp, Fox Sports, NineMSN, FetchTV, Coles, Woolworths, Trust Bank, and Westpac, among others. If you're looking for help developing an iOS app, drop me a line!

Get in touch:
[email protected]
github.com/chrishulbert
linkedin



 Subscribe via RSS