Transitioning From Objective C to Swift in 4 Steps - Without Rewriting The Existing Code

We started developing Skyscanner TravelPro in Objective C in March 2015. A couple of months later, when Swift 2.0 was released, we started to slowly introduce Swift. Fast forward to 8 months later - and 100% of the new code we write is in Swift. Without having rewritten all of our existing, working and tested Objective C code - there would have been little point in doing so.

There are many resources talking about how to decide whether to use Swift for a new project or not, best practices for writing Swift. However if you're in knee deep in an existing, often pretty large Objective C codebase, you will probably find this article useful. If not - one day you might bump into a codebase where you want to start using Swift: this article is a good start.

Here's a visual representation of how the codebase has changed in 10 months. Since November 2015 all new code is written in Swift, which now makes up about 10% of our 65,000 line codebase - and growing.

So what was our approach going from Objective C to Swift?

1. Start with a Simple Component

We decided to start as simple as we could: with some isolated classes that could be tested and used by themselves. The first few components we chose were simple UI controls, utility functions and extension methods on existing classes.

For example among the first Swift additions we added in was a simple String extension method that made localizing strings much more pleasant to read:

extension String {
    var localized: String! {
        let localizedString = NSLocalizedString(self, comment: "")
        return localizedString
    }
}

Interesting enough we could have implemented the same functionality using Objective C categories, however it never occurred to the team to use anything other then the good old NSLocalizedString(@"MyText", @""). With a new language, lots of new ideas surface. So from day one all our Swift strings are written in the tidier "MyText".localized format.

2. Using Existing Objective C Code From Swift

After writing a couple of standalone Swift components - and unit tests against them - we moved on to using our existing Objective C classes from Swift. Things started to get real.

To use any Objective C classes from Swift you need to define a Swift bridging header. This is a .h file where you define all your Objective C headers to "expose" for Swift to use. On top of the header itself, the build settings need to be changed for the compiler to pick this up. Once this is done, these Objective C classes are imported into the Swift world, and can be used easily.

When using Objective C classes from Swift then you will likely notice warnings saying pointer is missing a nullability type specifier. When Objective C code imported into Swift then the compiler checks for nullability compatibility - and if it doesn't find any information on nullability, then issues this warning. It does this check because in Swift nullability information is always explicitly declared, either with non nullable types or by using optionals.

The only changes we needed to make to our Objective C code was adding nullability information to the header to resolve the warnings issued by the compiler. To do so, we used the new _Nullable and _Nonnull annotations. This was something that only took a couple of hours - and made is think long and hard on what could, or could not be nil in our existing codebase.

For the most part this refactor involved changing lines of code like this:

// Original method signature in the .h file
@property (nonatomic, strong, readonly) THSession *session;

// New, Swift-friendly method signature
@property (nonatomic, strong, readonly) THSession * _Nullable session;

In case of method signatures with blocks, the changes were a bit more complex, but nothing unmanageable:

// Original method signature in the .h file
- (NSURLSessionDataTask *)updateWithSuccess: (void(^)())success
    error:( void(^)(NSError * error))error;

// New, Swift-friendly method signature
- (NSURLSessionDataTask * _Nullable)updateWithSuccess: (void(^ _Nullable )())success
    error:( void(^ _Nullable )(NSError * _Nonnull error))error;

3. Using Swift Code From Objective C

After having a couple of moderately complex Swift components using our Objective C classes, it was time to use these components from within Objective C. Using Swift components from Objective C code is much more straightforward, as there is no bridging header needed.

The only changes we had to make to our existing Swift files was inheriting from NSObject or adding the @objc attribute to classes we wanted to expose. There are some Swift specific classes that cannot be used from Objective C, like structures, tuples and generics and a few others. These limitations didn't affect us because we didn't want to expose any of the new structures to Objective C. The only exception, where we had to do a little extra work was enums. To use enums from Swift, in Swift they need to be specified with the Int value type:

@objc enum FlightCabinClass: Int {
    case Economy = 1, PremiumEconomy, Business, First, PrivateJet
}

3. (Re-)learn Unit Testing and Dependency Injection with Swift

Once we had some more complex components with dependencies, we hit an issue that wasn't obvious on how to best resolve. This issue was unit testing. Unlike Objective C, Swift does not support readwrite reflection. Put it simply: there is no OCMock equivalent in Swift, in fact mocking frameworks straight don't exist.

An example that caused us to scratch head it this. We wanted to test that when pressing the submit button on a page, the saveTrip method is invoked on the viewModel property of the view object. In Objective C, using OCMock, this could be tested in a similar way:

// Testing that when pressing the submit button, a method is invoked on the ViewModel
- (void)test_whenPressingSubmitButton_thenInvokesSaveTripOnViewModel {
    // given
    TripViewController *view = [TripViewController alloc] init];
    id viewModelMock = OCMPartialMock(view.viewModel); 
    OCMExpect([viewModelMock saveTrip]);
    
    // when
    [view.submitButton press];
    
    // then
    OCMVerifyAll(viewModelMock);
}

In Swift this approach would not work. In Objective C unit testing is usually done with the help of rich mocking frameworks like OCMock. Dependency injection is a good practice, but as OCMock makes unit testing very easy even without explicit dependency injection, most of our Objective C dependencies were implicit. In Swift however, dynamic mocking libraries like OCMock do not exist. In Swift the only way to write testable code is by making dependencies explicit and injectable. Once this is done, you have to write your own mocks to verify behavior.

Sticking with the previous example: in Swift it would need to be changed, so the viewModel can be passed in as a dependency to the view. This can be done by either having the viewModel implement a protocol, or by subclassing the viewModel itself. The test class needs to define the mock object that is being passed:

func test_whenPressingSubmitButton_thenInvokesSaveTripOnViewModel() {
   // given
   let viewModelMock = TripViewModelMock()
   let view = TripViewController(viewModelMock)
   
   // when
  view.submitButton.press()

   // then
   XCTAssertEqual(viewModelMock.saveTripInvoked)
}

class TripViewModelMock: TripViewModel {
        var saveTripInvoked = false
        
        override func saveTrip() {
            self.saveTripInvoked = true
        }
}

The Swift test code is visibly more verbose then the Objective version. However, the explicit dependency injection pattern forced us to decouple our code as much as we could. Before migrating to Swift, we thought that our Objective C code was pretty decoupled. However after writing a couple of weeks of Swift code, the difference between the "old" and "new" code was stark. Moving to Swift - and testing our code properly - made our codebase more loosely coupled then before.

Dive Deep in the Good Parts

After getting the hang of dependency injection and writing our own mocks, we got much deeper into Swift, and started to pick up some really neat techniques. In the previous example I showed how to re-create the OCMPartialMock functionality from Objective C. A cleaner approach would be to use pure mocks instead of partial mocks. In Swift a better way to write loosely coupled code is using protocols, and protocol oriented programming techniques. We picked up this really quickly and our code became more loosely coupled and more testable.

Then there's some new language features like the guard and defer, generics, error handling with do-catch, nested types, the where clause and the @testable keyword - and this is only touching the surface. Even though Swift is easy to get started with, there is plenty of depth to the language.

Apart from learning a new language, what else did we get out of moving over to Swift?

  • Easier to read code:
// Objective C
CGColorRef newColor = [[[UIColor blueColor] colorWithAlphaComponent:0.2] CGColor]];
[self updateLayerBackgroundColorWithColor: color];

// Same code in Swift
let newColor = UIColor.blueColor.colorWithAlphaComponent(0.2).CGColor
self.updateLayerBackgroundColorWithColor(newColor)

On the disadvantages side: there seem surprisingly few. One important one is that some of our third party dependencies building on the dynamic nature of Objective C like JSONModel aren't and won't be available in Swift. And the other big one is that now need to maintain our existing Objective C code, which means additional context switching - and motivation to continuously transform more of our Objective C code to Swift.

Of course Swift is still a new language that is heavily under development, and breaking changes coming late 2016. Despite all that all of our team agrees that moving our Objective C project to Swift has been great success. It resulted in cleaner architecture, easier to read code and more productivity then if we would have stayed all Objective C. More importantly: by doing a gradual change and not rewriting our "old" code, shifting from Objective C to Swift has not slown us down a bit.

(Note: I have since then written a follow up to this post: How We Migrated Our Objective C Projects to Swift – Step By Step).


Featured Pragmatic Engineer Jobs

  1. Senior DevOps Engineer at Polarsteps. Amsterdam.
  2. Senior Software Engineer at Ladder. $150-175K + equity. Palo Alto (CA) or Remote (US).
  3. Senior Software Engineer at GetYourGuide. Berlin, Germany.
  4. Senior MLOps Engineer at GetYourGuide. Berlin, Germany.
  5. Senior Software Engineer (Reporting) at CAST.AI. €72-96K + equity. Remote (Europe).
  6. Senior Software Engineer (Security) at CAST.AI. €60-90K + equity. Remote (Europe).
  7. Senior Sales Engineer at CAST.AI. Remote (Europe, US).
  8. Senior Frontend Developer at TalentBait. €60-80K + equity. Barcelona, Spain.
  9. Technical Lead at Ably. £95-120K + equity. London or Remote (UK).
  10. Senior Software Engineer, Missions at Ably. £80-100K + equity. Remote (UK).
  11. Software Engineer at Freshpaint. $130-210K + equity. Remote (US).
  12. Senior Software Engineer, Developer Ecosystems at Ably. £80-100K. Remote (UK).
  13. Senior Web Engineer, Activation at Ably. £75-85K. Remote (UK).
  14. Web Engineer at Ably. £70-75K. Remote (UK).
  15. Staff Software Engineer at Onaroll. $170-190K + equity. Remote (US).
  16. Staff Software Engineer at Deepset. Remote (US, Europe).

The above jobs score at least 10/12 on The Pragmatic Engineer Test. Browse more senior engineer and engineering leadership roles with great engineering cultures, or add your own on The Pragmatic Engineer Job board and apply to join The Pragmatic Engineer Talent Collective.

Want to get interesting opportunities from vetted tech companies? Sign up to The Pragmatic Engineer Talent Collective and get sent great opportunities - similar to the ones below without any obligation. You can be public or anonymous, and I’ll be curating the list of companies and people.

Are you hiring senior+ engineers or engineering managers? Apply to join The Pragmatic Engineer Talent Collective to contact world-class senior and above engineers and engineering managers/directors. Get vetted drops twice a month, from software engineers - full-stack, backend, mobile, frontend, data, ML - and managers currently working at Big Tech, high-growth startups, and places with strong engineering cultures. Apply here.