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

Compare your Objective-C and Swift code through time with Swoop

At Songkick, we’re busy converting our iOS app Objective-C codebase to Swift, and we built a tool called Swoop to help us track our progress.

We started to use Swift for our iOS app since November last year. That means new features and new tests are written in Swift. But how about our existing Objective-C code? We approached the conversion from Objective-C to Swift carefully. We have a small team and wanted to keep shipping new features, so we could not afford the risk of having major code changes. Instead, we started with smaller changes to the most problematic Objective-C code.

Our models and networking code were the first two areas that we actively convert into Swift, mainly because we use an old and unsupported network library. Early 2016, we pushed quite hard on these conversions and progressed excellently.

At that time, I was curious to understand the velocity of our progress. Maybe seeing it as a graph would be cool. This idea was then realized into a Ruby gem I named, Swoop.

Swift and Objective-C comparison reporter

Swoop compares and reports your Swift and Objective-C code through time. It goes through your git history, reads your Xcode project, compares them, and presents the information in a digestible representation.

To use Swoop, just install the gem using `’gem install swoop_report’`, and use the command with 2 required parameters :

  • Path to your Xcode project, and
  • Directory of your interest (the directory inside Xcode project)

Call the swoop command from your terminal like so :

`$ swoop –path ~/your_project/project.xcodeproj –dir Classes`

By default, it will present you a table of the last eight tags of your project, similar to the table below.

How it works

The diagram below explains how Swoop’s main classes work together.

sw_diagram

  1. It creates a `Project` using the path parameter.
  2. `TimeMachine` uses the project, and then figures out which git commits should be used based on the options provided.
  3. Once `TimeMachine` got the list of commits, it checkouts each one and starts the comparison process, which is broken down into :
    1. Selects the files that are inside the specified directory.
    2. `EntityParser` parses filtered Swift or Objective-C files and counts its classes, structs and extensions.
    3. Collates file information into a `Report`.
  4. All of the `Report`s are rendered by a chosen subclass of `Renderer`.

Below is the snippet of the Swoop’s main program :

# 1) create project with path
project = Project.new(@project_path, @dir_path)

# 2) create time machine with project and options
delorean = TimeMachine.new(project, @time_machine_options)

# 3) time machine checkouts for each commit
reports = []
delorean.travel do |proj, name, date|
  # 3.a) filter interested files
  files = proj.filepaths
  
  # 3.b) parse information from files
  entities = EntityParser.parse_files(files)
  
  # 3.c) put information in a report
  reports << Report.new(entities, name, date)
end

# 4) render array of reports as a table
renderer = TableRenderer.new(reports, "Swoop Report : '#{@dir_path}'")
renderer.render

Result

This is what our iOS app’s comparison report looks like :

sw_chart

Until now, our Swift code constitute roughly 35% of our whole codebase. From the graph, we can see that the number was vastly improved because of the work done around February until March. At that time, we were actively converting code to Swift. Then, the past three months it stagnated a bit because we changed our team goals and changed our focus to other projects.

After it worked for our iOS app, I ran Swoop on two other open source projects: Artsy’s eigen and WordPress’ iOS app.

Artsy’s Eigen

Last 8 minor versions of eigen :
`$ swoop –path eigen/Artsy.xcodeproj –dir Artsy –filter_tag ‘\d\.\d\.\d(-0)?$’ –render chart`

sw_artsy

WordPress for iOS

Last 12 major version of WordPress for iOS :
`$ swoop –path WordPress-iOS/WordPress/WordPress.xcodeproj –dir Classes –tags 12 –filter_tag ‘^\d.\d+$’ –render chart`

sw_wordpress

All in all, it works pretty well for our app and we plan to incorporate this into our continuous integration pipeline.

Onwards

We will need to test Swoop using more Xcode projects because sometimes it fails to do the job for projects that have directory changes in their git history. Also, we will aim for 100% coverage in the near future.

Any form of contributions are welcomed! Let us know if it doesn’t work for your project (it’ll be better if the project is publicly accessible). For more information on how to use and improve Swoop, please visit : https://github.com/ikhsan/swoop.

Posted in iOS