At DoorDash we recently migrated the codebase of our iOS Consumer and Dasher apps to Swift 3 from Swift 2. We decided to migrate the codebase after XCode 8.3 was released in March, which ended support for Swift 2.3. The newest versions of many third party libraries used by our apps are written in Swift 3, and since Swift 2 and Swift 3 modules cannot import each other, we weren’t able to upgrade to the newest version of many libraries. Additionally, Swift 3 has many improved features and syntactical improvements that we wanted to take advantage of.
Our codebases contain nearly 100,000 lines of Swift code which made the migration a non-trivial and challenging task. We want to share some steps we took to facilitate the migration to Swift 3 and what we learned from the process.
Preparation
Before we began we decided to do a code freeze of feature development so that we would not need to constantly update our migration branch with feature development changes.
We also realized that much of the initial work, such as updating code dependencies and project settings, would have to be done in a sequential fashion. However, we quickly realized that we could speed up the process by dividing the codebase into different subtasks to allow multiple developers to help with the migration effort simultaneously We use JIRA as our task tracking tool, so to aid with dividing up the steps of the migration process we decided to create a JIRA reference ticket along with sub-tasks to make it clear which task each developer was working on and to track the overall migration process.
Those subtasks divided up the codebase in alphabetical order by directory path.Finally, Swift 3 includes many naming convention improvements over Swift 2. We decided to follow the official Swift API Design Guidelines since it offers suggestions on standard best practices when writing Swift code. For example, when the first argument of a method can form a prepositional phrase with the method name, it should have an argument label.
func configureItemOptionCell(with itemOption: ItemOption)
We also came up with our own internal Swift Style guide that clarified additional style conventions such as spacing (indent with spaces instead of tabs) and guard statement usage.
Initial steps of the migration
Since migration steps are not easily parallelizable, one developer worked sequentially on the first few steps of the migration process. These steps took around two days to complete.
First, the developer updated external third party library dependencies. We use Cocoapods for library dependency management and thankfully all of our Swift dependencies were popular libraries that have Swift 3 versions, enabling us to update each dependency pod to the latest version with a simple change to the Podfile. If we had any dependencies that did not have Swift 3 versions, we would either need to remove the dependency or fork the pod repo and perform the Swift 3 upgrade on the code ourselves.
Next we updated the XCode project file. We decided to forgo using the migration tool since the tool makes a lot of changes that don’t automatically follow the API guidelines, such as having empty argument label for the first parameter of converted functions and access control replacements such as using fileprivate
when private
is sufficient. We also found that the migration tool made many changes that resulted in unnecessary casting between NSError
and Error
in our codebase. Another disadvantage of using the migration tool is that it makes changes to the entire project, which makes it hard to create pull requests to peer review code in chunks. However, using the migration tool is a perfectly valid option especially for apps that are divided up into smaller modules.
Since we didn’t use the migration tool, we used regular expressions to find and replace many Apple frameworks changes. For example, CGRectMake(0, 0, 100, 50)
can be changed to CGRect(x: 0, y: 0, width:100, height: 50)
with a simple regex find and replace.
Lastly, we created a main migration branch for these changes. We used the branch Swift3/MainMigration
, which was created for the individual subtasks that covered migrating directories within our app. After the work for each subtask was done, the branch for that subtask was merged into this main branch.
Migrating the codebase
We had five iOS developers work in parallel on the JIRA subtasks that divided up the codebase to apply the remaining migration changes. This process included migrating Swift files for each subtask to support Swift 3 and opening Github pull requests against the main migration branch for changes as they were finished. We also peer reviewed each PR to make sure they conformed with the Swift API guidelines and our style guide.
This entire process took a little over a week to finish.
Compiling, testing and releasing
After the first pass through of all the files, we still had around 100 compile errors. One iOS engineer fixed the remaining issues and made sure the app compiled properly. Many of the issues that remained after the first pass were because SourceKit often will not show errors in files until dependent errors in other files are resolved.
Before releasing, we also did a thorough test of all app functionality since the migration touched so many areas of our codebase. We fixed issues as they were found during testing. One common crash we encountered was due to UIKit button selectors being changed to include WithSender
in the selector name.
For example:
[DoordashDriver.BankDetailViewController didTapEditButtonWithSender:]: unrecognized selector sent to instance 0x7fbab0e55150
The fix was simply to add WithSender
to the name of the IBAction methods.
Our app next went through another round of testing where we distributed the app to employees internally as well as to beta users. This step was essential to help catch bugs and to reduce the likelihood of regression bugs going out with the release. After our internal testing and beta periods, we felt confident that our release was stable and we released our app through our normal release process.
But of course, a developer’s job has only just begun after an app has been released. Proper crash detection and network request monitoring through tools like New Relic is essential to catch post-release issues and crashes. Thankfully we did not encounter any critical post release bugs in our Swift migration release.
Common changes required
We encountered a number of common issues throughout the migration process. Here are a few of the most common changes that we had to make.
- Closure parameter labels — Swift 3 removed support for argument labels in function types. This requires the migration process to involve removing parameter labels from closures. This limitation of Swift 3 is discussed in Closure Parameter Names and Labels. In those cases, we simply removed the argument labels in the closures.
- Optional protocol functions — The function signatures for UIKit protocol delegate methods have changed for many classes like
UITableViewController
andUICollectionViewController
. The impact of the protocol name changes are not obvious since the code will compile fine without updating the functions since the functions are optional protocol functions. For example:func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath)
has changed to:func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
. Using XCode’s find and replace feature can help with catching and resolving many of these protocol name changes. - Variables that are assigned to implicitly unwrapped optionals will now have an optional type — This is related to the change in Abolish ImplicitlyUnwrappedOptional type. Implicitly unwrapped optionals are now only allowed at the top level and as function results so function argument types need to be modified. For example, in the following case we just had to change the implicitly unwrapped optional type to be an optional type instead:
#Swift Lint
We also decided to adopt Swift Lint during our Swift migration process to automate code conformance to our style guide. swiftlint autocorrect
will correct many issues (548 files for us!), but many issues such as file length violations can’t be auto-fixed. This can be addressed by using swiftlint:disable
comments to temporarily disable the violations and then coming back to fix these issues at a later time.
Final Thoughts and Tips
Ultimately, we’re very happy that we completed this migration. While it took time away from product development and required a lot of experimentation, our codebase is now up to date and more scalable thanks to using Swift 3 across all our apps.
If you choose to make the update from Swift 2 to Swift 3 be sure to read the API guideline documentation thoroughly to understand the best practices in naming conventions. Also, make sure there is a consensus on the team for Swift style before starting the migration process.
Additionally, if there are compile errors that are difficult to figure out, move on to other errors and go back to the skipped errors later. Many compile errors are due to dependent files having errors. In retrospect, we might have encountered fewer migration issues if we spent more preparation time to order the migration process in a way that migrated files that were dependencies first. If you do encounter errors, the compiler does a relatively good job of suggesting fixes, but pay attention to avoid unnecessary casting and to ensure that function signatures follow the Swift API guidelines.
Remember that proper communication and coordination during the entire effort is critical. We organized a meeting for all the developers involved before starting the migration so that everyone would be up to date on the entire process. We also made sure to sync up regularly during the migration to share what we learned and coordinate working on the remaining tasks.
Finally, try to limit code modifications to changes that are absolutely essential for the Swift 3 migration. Many times we were tempted to refactor code as we worked through the files for the migration changes but we decided that too many code changes at once can lead to hard-to-discover regression bugs. The migration changes are large enough as-is, so we decided it would be better to add tickets to our bug tracking tool in order to address refactoring changes in a future release.