Started From The Model Now We’re Here: A Swift 3 Migration Diary

This is a romanticised activity log of Swift 3 migration work that happened in November last year.

For context, Songkick’s iOS app consists of both Objective-C and Swift code. Before migrating, we have converted 40% of our Objective-C classes to Swift.1 These Swift files were needed to be upgraded to Swift 3 to comply with current and future Xcode releases.

We hope that you find this story useful. If you are still working on the migration, best of luck! First days are surely tough, but if you persevere through then you will reach the finish line sooner than you think.


Day 0

“Xcode 8.2 is the last release that will support Swift 2.3.”
— Xcode 8.2’s release notes

Sigh, I think this is it. I understand Apple’s aggressiveness, but I was a bit surprised for it to be forced this quick. I guess I will raise this requirement to my Product Manager (PM) so we can prioritise migration work this week.

I tried migrating once before when Swift 3 just came out. Xcode’s Swift Migrator tool converted many things, but still left many errors. When I fixed 3 errors, 10 new errors appeared. We ended up going with the easier route: migrating to Swift 2.3. It worked well at the time, but now Swift 2.3 will not be supported.

I guess I have to deal with those endless errors and try my best. Luckily, I saw Rob Napier’s tweet just before starting the work.

Hmmm, compared to my previous approach his approach is better in terms of predictability and control. Great, I will remove all the swift files from our main app target and call it a day.

Day 1 & Day 2

Day 1 starts with PM’s blessing to start the migration. Great, now I can take my time and strategise my approach.

Based on stories from other developers, this will take days if not weeks. This project won’t compile for days and that is fine. The reason I can make peace with an uncompilable project is this: Errors are fine as long as they are in Objective-C files. More on this later.

The strategy starts from the simplest, most inter-dependent, and most testable objects: the models. I add them to the app target and run the migration tool, one-or-two files at a time. Once all models are included, I repeat the same process to network classes since they only depend on models. Eventually, I manage to convert all of our networking code, including request objects and API caller objects.

At this point, errors on Objective-C files are fine because they are missing classes written in Swift. There always will be errors until all Swift files are included back in the app target. Our approach focuses on making all included Swift files error-free, so errors in Objective-C is acceptable and will be resolved later.

Day 2 is a downhill compared to day 1. Most of the work is handled by the Swift Migrator. Various classes were migrated in this order: view models, extension classes, views, and view controllers. In total 170 Swift files in the main app target are successfully migrated.

Migrator tool really helps this process but there are manual conversions needed to be done. Below are the notes from the first two days.

AnyObject -> Any

Swift 3 converts Objective-C’s id into Any, not AnyObject like what Swift 2 did. This affects most of our models because they deal a lot with JSON dictionaries.

For example, look at this struct in Swift 2

struct Event {
  let id: Int
  let type: String

  var analyticsProperties : [String : AnyObject] {
    return [
      "id": id,
      "type": type
    ]
  }
}

The migrator converts to Swift 3 into

// The migrator's wrong direction
var analyticsProperties : [String : AnyObject] {
  return [
    "id": id as AnyObject,
    "type": type as AnyObject
  ]
}

In Swift 3, AnyObject only applies for NSObject classes. The migrator casts to AnyObject is because Int and String are Swift structs, not Objective-C objects. Most of the fix related to this is just to manually rename AnyObject to Any.

// The manual fix
var analyticsProperties : [String : Any] {
  return [
    "id": id,
    "type": type
  ]
}

New Access Controls

Swift 3 introduces new access controls: private, fileprivate, internal, public, open. The visual below explains clearly the differences between each access control.

The Swift Migrator converts all private access to fileprivate. All fileprivate access are manually checked and changed back to private whenever possible. public and open were not used because we only have one main target, which is the app target. This will be revisited in the future once we start to modularise the app into frameworks for sharing between targets (e.g. main app, extensions, unit tests, and UI tests).

Closure as parameters is non-escaping as default

Closure passed as an argument now is non-escaping by default. Escaping closure is a closure that is passed as an argument that is invoked after the function returns. Migration tool misses some of our API calls. The fix is to add the @escaping annotation manually.

// from
func getArtistDetails(for artistId: Int, success: (ArtistDetailsResponse) -> Void, failure: (NSError) -> Void) -> Request {}

// to
func getArtistDetails(for artistId: Int, success: @escaping (ArtistDetailsResponse) -> Void, failure: @escaping (NSError) -> Void) -> Request {}

Status: Main target cannot be compiled, but all swift files are error-free

Day 3

Good thing that we write tests for most of our Swift code. These tests have proven to be crucial for the migration process.

Migrating test files is done in a similar fashion, test files are added to the test target and are run incrementally. Failing tests are ignored for now, as we are aiming only for successful compilation. Once all tests are included in the test target, all the failing tests are fixed. In total, 70 test files are migrated.

Status: Test target can be compiled

Day 4

Day 4 is all about cleaning up; resolving warnings and renaming methods to comply better to Swift 3 naming guide.

Lowercased enums

Most of Foundation’s and UIKit’s swift enums got converted by migrator. To make it consistent with our code base, all of our own swift enums are manually lowercased.

NSDate categories

Swift 3 has Date object as a struct not a class. Have a look at this Date‘s helper method.

extension Date {
  func apiFormat() -> String { // ... }
}

This does not automatically translate into Objective-C categories well because Date is a struct. To fix this, NSDate‘s method is added in its extension by calling Date‘s appropriate method.

extension NSDate {
    func apiFormat() -> Bool {
        return (self as Date).apiFormat()
    }
}

Warning: “incompatible Objective-C category definitions”

Last weird warning is the “incompatible Objective-C category definitions”. This is triggered when a class has a computed property in an extension. Objective-C seemed unhappy about it (although it got translated fine). To remove the warning, use class method instead of computed property.

extension Bundle {
  
  // This produces warning
  class var appVersion: String { // ... }
  
  // Changing the computed property to a method resolves the warning
  class func appVersion() -> String { // ... }

}

Status: Main target can be compiled

Started From The Model Now We’re Here

It took almost 5 working days to migrate all swift files to version 3.0. There are 203 files migrated, in which 133 files included in main target and 70 files in the test target.

Let’s hope for a better Swift 4.0 migration process. Swift team is aiming for source compatibility moving forward, so hoping for less manual work and fewer aggressive changes.


Other useful links on Swift 3 Migration


1. Check our previous post for more details on how we approach and track our code conversion from Objective-C to Swift

Posted in iOS