Tag Archives: Swift

Swift 3 Migration

In March, the iOS team at Omada Health began the process of migrating our app’s Swift code from Swift 2.3 to Swift 3. This was a large undertaking for a small team, and we would like to share some of the strategies used and the lessons we learned.

Why to migrate to Swift 3?

Our app was initially written in Objective C, and added Swift early on. By the summer of 2016, most new code was being written in Swift, although there is a considerable amount of Objective C remaining. We chose to postpone adopting Swift 3 for a few reasons: competing priorities, memories of the problems faced moving to Swift 2, and dependencies on libraries that had not yet released versions compatible with Swift 3.

However, as time when on we were aware that delaying migration would mean more problems later. Every line of Swift 2 code added would have to be converted. Many libraries and tools added support for Swift 3 and began dropping support for Swift 2. New releases of Xcode would no longer support Swift 2 at all, including the current release of Xcode. Delaying the migration only meant more Swift 2 code to convert.

Swift 3 Advantages

We would defer to other blogs and resources about more details on Swift 3. For us here is what was more important:

  1. More readable code
  2. Function named parameters

return Weight.humanValueStringForWeightValue(weightValue, includeSuffix: true)
return OPWeight.humanValueString(forWeight: weightValue, includeSuffix: true)
1. Lower cased enums
1. Use Date for bridging from NSDate, as it now can include timezone
1. Date is Comparable
1. is a value type, can be constant
2. Better tooling
3. Faster compiler
4. Able to use Struct on stack instead of Classes on heap
5. 77% size improvement for build product

See more on Swift evolution:
https://github.com/apple/swift-evolution

specifically NS Foundation migration: https://github.com/apple/swift-evolution/blob/master/proposals/0086-drop-foundation-ns.md

How we did it

The most basic approach might have been to simply update our libraries to Swift 3 versions, run Apple’s conversion tool on our project, fix errors left by the conversion tool until everything compiles, and then spend some amount of time manually testing the app to make sure we haven’t introduced bugs. We also considered hiring outside contractors to do the conversion.

We decided against this for several reasons. At Omada our engineering team has been strongly influenced by Extreme programming, a type of agile software development, and we prefer short development cycles and frequent releases. We wanted to avoid having our app in a broken, unreleasable state for an unknown length of time. We also wanted to continually monitor our progress and estimate how much work was remaining. And we don’t have a dedicated QA team to find all the bugs. We use a Jenkins server as our continuous integration system to run tests on every checkin.

Gradual Approach

We wanted to have an incremental approach to use tests to help us with the conversion, and we wanted to take advantage of Swift 3 improvements to improve the code while minimizing the scope and risk of change.

We prepared for the migration by adding a basic suite of acceptance tests. These tests are written using KIF to simulate user interactions with the app running against a development api server. This app had been tested in this way before but the previous developers abandoned those tests after they became too brittle and difficult to maintain. Our new tests were very basic, mostly just navigating to the major screens and making sure the app does not crash. With just a few tests we were able to cover about 20% of our code. This complemented a suite of around 1500 unit tests, and would let us sanity check our converted code even before we finished converting the unit tests.

We spiked on converting a few classes to Swift 3 to get a sense of how fast we could convert them, and to learn what kinds of problems might come up. We added some code metrics reporting to our CI process, and were able to estimate that it would take 2 to 6 weeks to do the migration.

When we were ready to start the migration we began by creating a new dynamic framework, allowing us to break our large app down into 2 units:

  • A Swift-only framework with no dependencies on external libraries or the rest of our application, and no bridging headers.
  • The main app that loads our complied Swift framework and builds the rest of our Swift and Objective C code.

With no dependencies or bridging headers the Swift framework complied very fast, and changes to the main app would not require recompilation of the Swift framework. We created a spreadsheet listing all of the Swift files in our app, and a swift3 conversion branch of our git repo. Then we started moving one file at a time into the Swift framework beginning with the smallest most isolated classes, adjusting access control levels of the classes and methods and importing our framework as necessary. After each commit we rebased the swift3 branch on master, converted the moved code to Swift 3, and marked the progress on our spreadsheet.

Through this process we found a lot of dead code that we could just delete! We also found classes that were difficult to move because of their dependencies. Sometimes these classes could be easily refactored to break those dependencies. In other cases we just noted the problem on our spreadsheet and moved on to lower-hanging fruit. We made several passes over our list and it became easier to move more classes as their dependencies had been moved previously. We also began using more sophisticated dependency-breaking techniques. In some places we extracted new protocols that could be defined in our Swift framework and implemented in the main target. In other cases we could move most of a class to the Swift framework and create an extension in the main target.

For example if we had a ProgressViewModel that depends on a Participant for some data, and for whatever reason we don’t want to move Participant into our framework:

class Participant {

func firstName() -> String {
return "Matt"
}

func allWeights() -> [Double] {
return [210, 207, 208, 204, 201]
}
}

class ProgressViewModel {

let firstName: String
let weights: [Double]

init(participant: Participant) {
firstName = participant.firstName()
weights = participant.allWeights()
}
}

We might remove the dependency on Participant from the definition of ProgressViewModel so that it could be easily moved.

class ProgressViewModel {

let firstName: String
let weights: [Double]

init(firstName: String, weights: [Double]) {
self.firstName = firstName
self.weights = weights
}
}

Then we can add an extension of ProgressViewModel in the main app with a convenience initializer that maintains compatibility with our existing code:

import SwiftOmada

extension ProgressViewModel {
convenience init(participant: Participant) {
self.init(firstName: participant.firstName(), weights: participant.allWeights())
}
}

After a while we added an additional test target and began moving unit tests for some of the Swift framework’s classes into it, converting those tests to Swift 3 to ensure we learned about any difficulties we might encounter testing with Swift 3. We added a jenkins job to build those tests on every push to our Swift 3 branch.

We found this process to have a lot of benefits. We were always aware of how much work was left to do, and we could tell how much work we had accomplished which kept the migration from feeling like an endless slog. We felt confident that we were not introducing new bugs that we would have to hunt down later. We were also knew that it would be possible to interrupt our migration if there was an urgent need to release a bug fix.

As the migration project progressed it became harder to move individual Swift files into the new target. At this point we had around 60 Swift files with too many dependencies to deal with on their own. At this point we began looking for a different strategy.

Revised strategy

We tried using tools that generate charts to visualize the graph of our app’s dependencies. They mostly showed what we expected: a big, tangled ball at the center of our application. However there were several overlapping clusters, and we hoped that by cutting in the right places we could separate the ball into 5 or 6 manageable chunks. At that point we would remove all of the code from the main target and add back only what was required to launch the app. Then we would add one chunk at a time, converting each chunk to Swift 3 as we went.

Again we made a spreadsheet listing each file in our main target, which chunk it was a member of, and its status. We observed that these chunks were roughly related to the main view controllers of our app, and often these view controllers were passing objects to each other after being loaded from storyboards, during segues, etc. To address this we started using the dependency injection framework Swinject and its companion SwinjectStoryboard, which one of our developers had used successfully on previous projects. Swinject is used to define a container which can resolve dependencies, and SwinjectStoryboard hooks into the process of instantiating a view controller from a storyboard to provide it with the objects it needs.

import Swinject
import SwinjectStoryboard

extension SwinjectStoryboard {

class func setup() {

defaultContainer.register(UIApplicationAdapter.self) { r in
UIApplication.shared
}.inObjectScope(.container)
defaultContainer.register(OPLoginServiceAdapter.self) { r in
let service = OPLoginService()
service.analyticsService = r.resolve(OPAnalyticsServiceAdapter.self)!
return service
}.inObjectScope(.container)
...
defaultContainer.storyboardInitCompleted(OPProgressViewController.self) { r, c in
c.account = r.resolve(OPAccount.self)!
c.accountManager = r.resolve(OPAccountManagerAdapter.self)!
c.accountUpdater = r.resolve(AccountUpdaterAdapter.self)!
}
}

In other places we changed our classes to use key value coding to set properties and performSelector to invoke methods.

Along with dependency injection we used key value coding to access coupled properties that were too hard to separate. For example when one view controller sets properties for another one in prepare for segue method:

[self.personalViewController setValue:self.messageBus forKey:@"messageBus"];

or when one model needs to access deep a property of another

let someValue = model.value(forKeyPath:"list.property.leaf")

When we need to call a method that we do not want to import we use performSelector:

[account performSelector:@selector(updateMeals)];

Release

Once we finished converting all of the Swift code in our app to Swift 3, we made a new beta release with TestFlight and distributed it to our coworkers. We added a few small features and visible bug fixes with the Swift 3 migration to make it an interesting release for users. While we waited for feedback about the beta, we finished converting the remaining unit tests to Swift 3. After about a week we felt ready to release to the App Store.

Conclusion

Overall it took us 3 developers working full time for 4 weeks to convert roughly 35,000 lines, within our estimated 2-6 weeks work. Our release had a 5 star average review, up from a 4 star average for all previous releases. Our test coverage increased from 50% to 64%, and we learned more about how our code works and structure of the app.

Some technical improvements to our codebase:

  • Swift Framework: more modular design
  • Swift Unit Specs: Can be executed faster without loading the app.
  • Date() to expose bugs coming from the use NSDate without timezone and calendar. Required us to be explicit about Gregorian calendar and current timezone.
  • Open Source: We started to use open source libraries for dates and dependency injection (SwiftDate and Swinject)
  • Cleaner design: Remove monkey patched Foundation and UIKit dependencies. We had to add extensions to Date temporarily while migrating to keep compiler happy.
  • Multiple suites of tests helped us to verify the conversion and keep the existing quality
  1. Unit Specs – fast command line only
  2. Unit Tests – load the main app and UIKit
  3. Legacy Tests – for Objective-C code
  4. Acceptance Tests: a suite of UI tests connecting to the real server running locally in development mode

In hindsight our conversion process would have been easier if we had started the conversion sooner to have less code to convert, modularized the application and started using dependency injection earlier and more consistently.

To review the steps of conversion:

  1. Break down large app into 2 units: dynamic framework and the app.
  2. Create Swift only framework, no Objective-C to have faster compilation with no bridging headers
  3. Make the Main app load the compiled Swift framework and build with Objective-C and the rest of Swift code.
  4. Parallelize work for conversion
  5. Migrate into swift3 branch one class at a time, that compiles on Swift 3 into a framework. We used a Google Spreadsheet to list all Swift code with number of lines and assign it to a developer.
  6. Migrate Tests into Unit Specs to have tests passing validating the code converted
  7. Move more classes until it became too difficult.
  8. Build “naked” app by removing all Swift 2 classes, to build with with only Objective-C compilation linked to Swift 3 framework
  9. Break down dependencies using key value coding and selector strings
  10. Use dependency injection
  11. Re-add Swift classes to the main app as we migrate each one to Swift 3. Another Google Spreadsheet was used here to “divide and conquer” files between multiple developers.
  12. Estimate work by:
  13. Counting classes and lines of code
  14. Converting a few typical classes and tests