The Great Swift 3 Migration

Written by: on December 14, 2016

Xcode 8.2 is released, and with it the announcement that it is the final version to support Swift 2.3. If your project hasn’t been migrated to Swift 3 yet, it’s time to join the Great Swift 3 Migration. Having just traversed a Swift 2 to 3 migration on a large project, my team took notes on the thornier parts of what it takes to keep pace with Swift.

Strategy

Xcode 8 provides a migration assistant that will take you from prior versions of Swift to version 2.3 or 3. You’ll be presented with a dialog to initiate the assistant if you open an Xcode 7 project in Xcode 8, or you can kick off the process directly with Edit -> Convert -> To Current Swift Syntax…

Do have a read through Apple’s Migration Guide which may help you spot known issues when they arise, and generally might help you tune your mind for the task ahead.

Before you get started, check that each of your third-party Swift dependencies has been migrated—otherwise those dependencies will stand as a roadblock to your migration intentions. At the time of our migration, Alamofire hadn’t released a Swift 3-compatible version, but they did have a Swift 3 branch in the works that we could point our project to.

Since we are also absorbing the impact of moving our project to the iOS 10 SDK which brings its own set of changes, we’ll migrate to Swift 2.3, the iOS 10 SDK, and Xcode 8 as a first step. This way we have a checkpoint after which we can run our tests and do some QA to ensure we haven’t had any regressions. Then we’ll run the migration assistant to go from Swift 2.3 to 3 and focus solely on syntax changes. Our goal is to get the project converted to Swift 3 with absolutely zero regressions—we’ll justify a little more time spent rather than risk a crash in production.

Schedule

A large-scale migration will span multiple days, and you won’t want any significant development efforts moving forward in the same codebase until everything is settled. Make sure you plan around that. We found that a two-person team was the right size to slog through the work. That’s the most I would assign to the migration, although it could speed things along to have others help investigate particular issues.

Protect the Source

We were very mindful about what we checked into Git and when. We tried our best to handle particular categories of issues in large swoops so that our commit history is easier to review.

Here’s how we managed branches during the migration:

  1. We began with a swift23Migration branch which was merged into development once we were clean-compiling on Xcode 8 with Swift 2.3 and the iOS 10 SDK.
  2. We made a swift3AutoConvert branch off of development and ran the migration assistant on it to go from Swift 2.3 to 3. This gave us a non-building baseline to compare our manual changes to.
  3. We made a swift3ManualConvert branch off of the swift3AutoConvert branch. This is the branch in which most of our work was done.
  4. When the swift3ManualConvert branch was ready, we submitted a pull request into the swift3AutoConvert branch to review our manual changes.
  5. Finally, we submitted a pull request from swift3AutoConvert into development so we could review all of the changes together. This approach gave us a second pass at reviewing our manual changes.

By the Numbers

When we migrated 100K lines of Swift code in a mixed Swift/Objective-C project that’s more than double that in size, changes included approximately:

  • 400 LOC auto-migrating to Swift 2.3, the iOS 10 SDK, and Xcode 8
  • 4500 LOC auto-migrating to Swift 3
  • 4000 LOC worth of manual fixes to build and run with Swift 3

That’s a huge delta just to maintain pace with Swift! Let’s go over some of what was auto-migrated as well as outtakes from the manual migration effort we put in.

Auto-migrate to Swift 2.3, iOS 10 SDK, Xcode8

Upon opening our project in Xcode 8 we chose the option to migrate from Swift 2.1 to 2.3. Following are some of the changes we encountered.

Multiple if/let statements require let on each condition

super.awakeFromNib() Now Required

We noticed lots of warnings related to not calling super.awakeFromNib() because the iOS 10 SDK makes it clear we are meant to call super.

Xcode 8 Code Signing

One benefit of Xcode 8 is much-improved code signing which let us specify profiles by name. You can read more about that in Code Signing in Xcode 8 by our resident provisioning expert Jay Graves.

New WCSessionDelegate Methods

On the WatchKit front, we had to add these new required WCSessionDelegate methods which are introduced to support multiple Apple Watch pairing:

We didn’t plan to work on multiple watch pairing at this time so we left ourselves TODO comments in the body of those methods.

Push Notifications are Off!

The Push Notifications option in Capabilities was in the off position because the iOS 10 SDK requires we add the push notification entitlement to our target’s entitlements file. This should be easily fixed by toggling the switch in Capabilities. We found that the entitlement added to the file was given a value for “development” and our push environment always registers as “production”, so we manually adjusted the entitlements file.

Apart from futzing with Push Notifications settings, migrating to Swift 2.3 and the iOS 10 SDK went smoothly. We checked-in all of our changes and generated a build for QA to test.

Auto-Migrate to Swift 3

Apple suggests: “If you need to apply any workarounds, discard the changes that you accepted from the migration assistant earlier, apply the workarounds, and invoke the assistant manually to re-try the conversion from the start.” An example of something you can be proactive about changing is type names that collide with renamed Foundation types. We had a class called Operation which post-migration conflicted with NSOperation being renamed Operation. After we had taken care of several of these Swift 2-compatible adjustments, we proceeded to run the auto-migrator a final time.

Note that you can run the migration assistant multiple times without discarding the prior-run’s changes. In our attempt to run it a second time we decided that this wasn’t an approach suitable for a project the size of ours. The clunkiness and slowness of the migration assistant created a feedback loop that was too long, and we were noticing things the migration assistant did that we were not happy with.

A prime example of undesirable migration assistant behavior is generated operator overloads to account for optional comparison changes in Swift 3. Here’s an example of a method that was added to the top of one of our class files:

This method adds functionality for comparing optionals to each other and also optionals to nil that was implicit in Swift 2, and without it we need to unwrap our Comparables prior to comparing them. While we appreciate the gesture, we preferred to refactor our code and be explicit about unwrapping our optionals prior to making comparisons.

One thing the migration assistant does very well is handle changing how we call system APIs in Swift 3 style. For instance:

Became:

We really enjoyed how this extensive API cleanup made our code much more readable and elegant.

Now that we had taken the auto-migration process as far as we could, and took lots of notes on what changes it made, we were ready to begin our manual migration.

Manual Migration

In their migration guide, Apple says “While the migrator will take care of many mechanical changes for you, it is likely that you will need to make more manual changes to be able to build the project after applying the migrator changes.At this point in the process we had many more manual changes to make.

In our note-taking exercise we tried to identify regular patterns of issues, and sometimes we made regular expressions to perform a project-wide search for code matching those patterns. This allowed us to take large steps towards reducing compile errors as opposed to fixing individual issues and trying to recompile.

Feedback Loops

Large mixed Swift/Objective-C projects compile particularly slowly—ours takes about 14 minutes to do a clean build on one of our typical 2013 MacBook Pros. This makes for long feedback cycles, and we had to favor making broader changes between compile attempts to offset the slow compile times when we’d usually prefer to have smaller iterations. This is basically the same challenge that kept us from wanting to run the migration assistant multiple times.

In no particular order, following are the most noteworthy issues we handled.

private -> fileprivate

The migration assistant changed our private methods and variables to fileprivate which is functionally equivalent but what we really wanted semantically when we wrote those declarations is the equivalent of the Swift 3 private access modifier. We did a project-wide search/replace to change the fileprivate instances back to private. I have heard on the Runtime podcast there are discussions around reverting this change and removing the fileprivate access level with Swift 4—we’ll see what happens.

Underscores in Method Signatures Everywhere

Because Swift 3 style often prefers to not label the first method parameter, the migration assistant changed methods to suppress the first parameter label. This was mostly an improvement at the call site over pattens we had before such as:

Which became less redundant:

There were, however, instances where we did not prefer the label-free conversion where the Swift API Design Guidelines suggest a first parameter label. We fixed many of those instances. The above example might be better as:

Optional Handling

Because optional handling from Objective-C APIs to Swift was improved, we found cases where the migration assistant added code to explicitly unwrap instances that previously didn’t require the ! operator. This exposed some places where we needed to perform proper optional handling, so we refactored accordingly. There were other cases where the migrator decided to force-unwrap expressions that could not evaluate to optional. In those cases, we removed the ! operator. We preferred to add nullability tags to related Objective-C headers, and then if that wasn’t tenable, we put in explicit optional handling on the Swift side.

Objective-C Categories

Our Objective-C categories were no longer implicitly available to Swift, and we often had to cast back to an Objective-C type to access those category methods.

Trailing Closure Syntax

While not a compile issue, we strongly prefer trailing closure syntax for methods that take one closure, and the migration assistant is not able to generate code that way. We changed code that migrated with closure parameters:

To use trailing closure syntax:

AnyObject -> Any

NSDictionarys from Objective-C used to import to Swift as [NSObject, AnyObject] and now are imported as [AnyHashable, Any]. We felt this impact the most in our JSON parsing utility where we need to change to use Any instead of AnyObject.

Open Classes

Some of our classes were marked with the open keyword which is meant to indicate they are subclassable outside of the module they are defined in. Most of the time we changed these class to be final because we had no intention allowing those classes to be extended or be accessible outside the module.

@escaping

Because closures passed as parameters are now non-escaping by default, basically meaning that you can’t hold onto them after the method terminates, we had to add @escaping to closure parameters in lots of places.

Generic NSFetchedRequest

We needed to modify all of our NSFetchedRequests to be specialized with generics. There were many instances of these.

(indexPath as NSIndexPath)

We had many cases where the migration assistant wrote code for us like this:

When really we just need:

if let, where

Conditional if/let conditions with a where clause no longer have the where keyword. The migrator took care of many of these, but the whitespace handling was sloppy:

Became:

default

We had a smattering of enum cases named Default. Enum cases now start with lowercase letters so we ended up with several backtick-surrounded `default` cases. We decided to rename those because we do not like naming things the same as keywords.

as CFString as CFString as CFString…

We had a runaway type conversion on casting to CFString. The solution ended up to be to remove the extra casts.

Collection Indices

Instead of calling successor() on an Index, we needed to call index(after:) on the related collection due to a change in how collection indices work in Swift 3. This was mostly isolated in our Collection extension helper class. The migrator left us with this:

Which we changed to:

// FIXME:

There were times when we decided to defer more complex refactoring in favor of making a faster dash towards having a compiling project. We placed FIXME comments in those instances, and we were able to track those project-wide because we use XcodeIssueGenerator.

In the end we felt cautiously optimistic we had handled everything well through sheer determination to get it right. All of our unit tests were passing and later QA would find just one regression related to optional unwrapping.

Being Swift 3

Keeping our code Swifty as we have carried it through releases starting at version 1.2 has been one of our primary interests, and has at times felt like trying to match the airspeed velocity of an unladen swallow. With the amount of Swift code we have as early adopters of the language, one area that has suffered is method style. My teammate Ada Turner wrote Swift 3 Declarative Programming after reflecting on the Swift API Design Guidelines—we try our best to exemplify those guidelines in our coding style. We couldn’t clean up our entire codebase but we did fix some of our favorite and most oft-used methods. For instance, we would like to go through the project and fix Objective-C style methods that look like this at the call site:

To methods that look more “Swift 3” like this:

Over time we hope to iterate on fixing our method signatures for greater project consistency.

Onward!

This has been a story about tackling the most harrowing of Swift migrations—we hope it helps you in your effort. We had a much easier time with our smaller and pure-Swift projects. I won’t go so far as to say we had fun migrating to Swift 3, but we did get into a groove with it and it’s satisfying to be able to write Swift 3 code today. See you on the other side!

Sean Coleman

Sean Coleman

Sean Coleman is a Technology Lead at POSSIBLE Mobile where he leads multi-platform projects with large teams for well-known brands. Outside of writing native apps for Apple platforms you’ll find Sean playing with his two kids or hitting balls on the tennis court.
Article


Add your voice to the discussion: