Swift 3 Migration

Are you looking down the barrel of a necessary Swift 3 migration and groaning? Well, having recently reached the other side of that journey with a large codebase (40KLOC of Swift, excluding pods/carthage/etc), I found it to be difficult enough that it deserves a write-up, to hopefully spare the reader a landmine or two.

Xcode 8 and Swift 2.3

Firstly, you'll want to migrate to Swift 2.3. This is presented as an option when you open your project in Xcode 8 for the first time. Don't do what I did and migrate from 2.2 (Xcode 7) to 3 in one step - its simpler to take it in smaller steps.

2.3 will give you access to all the newest iOS10 APIs, and is as such a good stop-gap for now. Please note that Xcode 8.2 is the last to support 2.3 and after that, you'll need to migrate to Swift 3 syntax.

Xcode 8.0 is quite buggy - I recommend skipping it. In fact, as of writing, 8.1 just came out so this information is possibly redundant (sorry, this article took a while to write). 8.1 still has the overly-verbose logging issue which renders the simulator console almost unusable however - hopefully in 8.2 this will be resolved. However there are no longer blocking issues in 8.1 so I recommend using it.

Migrating

If you are part of a multiple-member team, I recommend getting one person's entire time set aside solely for a migration, in order to migrate as quickly as possible so that you don't have a nightmare of merge conflicts. I also recommend choosing a week when other team members aren't making large refactors/additions to the codebase too, again for the sake of merge conflicts.

Alright, jump in and run Xcode automatic migrator on your codebase. I recommend eyeballing all the migrations it makes - in my experience it did a poor job and many simple mistakes were made. This will take you quite a while unfortunately.

Once you've done this, try building your project. Don't be upset if you get many errors, this is simply a result of Xcode's migrator which isn't fantastic. I found that the error view in Xcode was often misleading regarding where the code wasn't compiling, and had to resort to the Xcode error logs (command+8) to find the true culprit of these problems. I recommend you do the same if you find any errors that don't make sense when looking at the code.

In the process of you fixing many errors, compiling, then finding more errors, and so on for many hours, you'll find that Xcode's indexing slows you down more than it helps. At this stage of the process, I found that disabling indexing really sped up the workflow. Run this from the terminal and restart Xcode:

defaults write com.apple.dt.XCode IDEIndexDisable 1

To re-enable later on, run the same command with 0 at the end.

Tests might not work upon first migrating - feel free to postpone solving this issue until after the main git merge to master has succeeded. The aim of the game is to get merged quickly to avoid conflicts with your other team members.

Dry run

If you have a really large codebase with many team members and are concerned about merge conflicts, you may want to consider doing a 'dry run' of the conversion to find parts that convert poorly. You can then refactor code such as this on your main (swift 2.3) branch of code such that it'll convert more easily when the time comes to jump in and do the full conversion. For instance, closures that have named arguments don't port across easily, and can be changed in the Swift 2 branch easily.

Another advantage of this technique is that when it comes time to do the full conversion, you can pull the latest from your master branch, then do the migration more quickly the second time around, and limit the length of time your Swift 3 branch exists for, thus minimising merge conflicts.

Pods / Carthage libraries

In the Swift 2.3 step in your migration, I recommend upgrading to the very-latest-but-prior-to-swift-3 version of all your dependencies/pods/carthage frameworks. Well managed open source projects, such as ReactiveCocoa, have subtle changes for the Swift 3 versions, and use of incompatible APIs in the prior version is marked as deprecated. Check all such warnings and migrate to the newest code style so that when you jump to the Swift 3 versions of such APIs, you will have fewer updates to make.

Also take stock of which libraries will need upgrading. Pure Objective-C frameworks will not need upgrading at all. For each of the Swift ones, check to see if there is a version for Swift 3 that has been released. If there hasn't been a Swift 3 version released, you have a few options:

  • Take the code out of the pod/carthage, and place it directly in your code, then migrate it alongside your own code.
  • Migrate to a different library that has the same purpose and also has a Swift 3 compatible version. I'd do this migration on your Swift 2 branch first, iron out the kinks, and merge that in before tackling the Swift 3 migration.
  • Rewrite your code so as to not use the library at all and remove it. If you're lucky, this might be a feasible option.

Time in lieu

You may consider letting one of your team members perform the final migration over a weekend to minimise merge conflicts, and allow them to take a couple days off in lieu the following week. Of course, do all you can to prepare for this ahead of time, such as migrating to 2.3 beforehand and preparing your strategy for your libraries.

Beware

In no particular order, some issues to be aware of (some of these may be resolved in Xcode 8.1):

Xcode 8 is buggy: communication between Xcode and the simulator are unreliable. Don't be concerned when this occurs, it's not just you.

Incremental compilation hasn't improved: usually you'll find that once your codebase gets to a certain size, it breaks down and any change will do a full recompile.

If you cast an NSObject to AnyObject via foo as? AnyObject, it returns a double-wrapped optional, so do foo as AnyObject instead.

var foo: NSObject? = nil; let bar = foo as AnyObject returns a non-optional AnyObject that is actually nil, causing runtime crashes.

Casting NSDictionary to AnyObject and back fails if you use as. Solution: Don't use NSDictionary anywhere, use [AnyHashable: Any] instead.

Any ObjC code that takes a NSDictionary will take a [AnyHashable: Any] from Swift. This will automatically bridge across from Int/Double/etc to NSNumber, String to NSString, etc. However any Swift-only types such as enums will also be happily passed across and cause runtime crashes. And the migrator will happily migrate some things attribute dictionaries across to using modern enums and trigger these issues.

UIControlState.Normal migrates to becomes UIControlState() often.

Swift 3 segfaults often - look in Report Navigator command+8 to find errors in code

Speed

Our biggest motivation for migrating from Swift 2 ASAP was our crippling compilation speed, and unfortunately compilation is still slow in Swift 3: on the latest rMBP15, it takes 7 mins for 40KLOC. However, there is a clever trick you can use:

Add the user-defined build setting SWIFT_WHOLE_MODULE_OPTIMIZATION = YES and magically our compiles are now down to 65s. This has been verified to work with other friends of mine with similar sized codebases. I have no idea if this takes precedence over the normal whole module optimisation setting, however it does seem to work.

One drawback I've noticed is that a subproject doesn't compile properly (if you have these), with SWIFT_WHOLE_MODULE_OPTIMIZATION enabled, the compile will simply never complete, as though it has gone into a while(true);. So if that happens to you, temporarily disable the build setting. It's unfortunate that we're at version 3 of the language and still having teething problems like these.

Hope this helps you get over the hump!

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