If you are a developer of a native iOS app written in Objective-C, the topic has likely come up whether it is worth switching your app to use Swift, and if so, when, and how. TripAdvisor is no exception, and with hundreds of thousands of lines of Objective-C code in our flagship app, we have had a huge decision to make on this topic.
It is unrealistic for TripAdvisor to stop development on new features to rewrite the entirety of the codebase, but we have examined the possibility of coexisting legacy Objective-C code and new Swift code, perhaps indefinitely. We’ve also carefully weighed what a successful migration plan looks like. Lastly, we have retained the option that, following an evaluation period, the right solution for us is to stay predominantly on Objective-C.
Swift, first introduced at Apple’s WWDC 2014 conference, offers a language syntax far more inviting to Java and Python developers than Objective-C. Code boilerplate is often reduced, especially with no need for separate header/implementation files. Swift’s collection and tuple APIs are built from the ground up with strong typing, allowing more typecasting issues to be caught at compile time. The compiler warns whenever a variable could be made immutable but is not, which can help make concurrently-executing code safer. Objective-C has been infamous for subtle bugs caused by passing a message to nil but failing silently. Swift bakes in support for optionals, to make it clear when nil must be checked in code to avoid a deterministic runtime error.
Lastly, TripAdvisor’s server-side codebase is predominantly Java (including the REST API implementation used by the native app), and the transition from Java to Swift is much simpler than the transition from Java to Objective-C. It would be amazing to reach a point where any Java developer at TripAdvisor can read both our native iOS and native Android code and not have language syntax be a barrier to understanding the code.
Testing the waters
TripAdvisor decided to start the migration effort with the place in our codebase least likely to introduce bugs in the app: our automated test suite. You read that correctly; we are testing our Objective-C code with a suite of tests written in Swift.
It actually makes perfect sense. Automated tests need to do all sorts of complex object mocking, subclassing, and method swizzling in order to run deterministically. These are all things that test the limits of Objective-C/Swift interoperability. That interoperability had better be rock solid if we are to start moving over our primary source code.
Furthermore, to attempt such a large rewrite of live app source code, our codebase needs to first have excellent code coverage. TripAdvisor’s culture strongly values developers doing their own QA and writing their own test cases- rather than having a separate QA team do it for them. So, every iOS developer on the team is now empowered to write Swift code, in every project they do, simply by writing test cases. Our testing stack includes KIF for integration level testing, FBSnapshotTestCase for UI view visual inspection, and standard XCTest cases, with OCMock, for unit testing. A large suite of test cases in Swift builds up confidence to undertake rewrites of large view controllers and views from Objective-C to Swift, with less of a need to worry about hitting every line of code via time-consuming manual testing.
The first few tests did take a bit longer than normal to write in Swift rather than Objective-C, but once some good common patterns are established, it becomes simple for any developer to add new tests in Swift.
Snags and workarounds
We did hit a few bumps along the way with changes to our testing infrastructure, but quickly found reasonable workarounds for all of them:
Object mocking is more difficult in Swift
Objective-C object mocking frameworks are built on the ability to change method implementations at runtime. Swift intentionally gives much less freedom to do so. A few options exist.
One is to slightly rewrite the main app code to allow the tests to invoke dummy implementations. The blog post http://blog.eliperkins.me/mocks-in-swift-via-protocols describes an example, where the main app code defines a protocol, and takes in an object that implements the protocol. The main app and the test implement the protocol in two different ways. Languages such as Java have long made use of this sort of production code refactoring for test purposes, and Swift is no different.
A second option is to fall back to Objective-C just when object mocking is needed. TripAdvisor’s Swift tests call into a factory class written in Objective-C, linked via a bridging header, and each method in the factory returns an object created with OCMock based on some minimal set of varying parameters. These objects are fully useable within the Swift test code from that point forward. This option keeps the main app code completely free of code added for testing purposes.
Every once in a while, the time comes when you just want to change a method implementation manually for test purposes. Method swizzling allows you to swap out the implementation of any method with some other implementation, for any code running in the process. This is completely dangerous to do in normal app execution, but quite helpful when writing automated tests.
Your test code may be hitting third party code directly, which is much more difficult to rewrite for testability. For example, TripAdvisor had a chunk of source code that was hitting ALAssetsLibrary.authorizationStatus(), a static method that sometimes pops a permissions prompt on the screen, depending on whether access to photos has previously been granted or not. The test was failing only for the times when the permissions prompt pops up. We could have modified the main codebase and wrapped the checks in #ifndef TEST, but that isn’t always an option if the code is coming from a third party.
It is far simpler to just swizzle the method, and move on. Method swizzling is still possible in Swift, under a limited set of circumstances: namely when the method being swizzled has the dynamic attribute, or when the class being swizzled is an Objective-C class. The article https://www.uraimo.com/2015/10/23/effective-method-swizzling-with-swift/ has a good description of the circumstances under which swizzling is still permitted in Swift.
So, in the short term, while using Swift to test Objective-C code, swizzling from Swift test code works perfectly well. As more of the main app codebase and bundled libraries move to pure Swift, method swizzling from test code will encounter hurdles whenever the dynamic attribute is not present.
No compiler defines
To get KIF up and running, we had to define extensions to XCTestCase and KIFTestActor, replacing the compiler defines used in Objective-C. See http://natashatherobot.com/kif-swift-feature-testing/ for an example of the type of workaround that is needed.
Any other compiler defines that are typically used in Objective-C, e.g. to wrap around NSLog, also need to be rewritten for Swift.
Swift tooling has a ways to go
If I use Xcode’s Refactor tooling to rename an Objective-C method, the corresponding Swift methods do not get renamed, and compilation simply breaks. Manual text find/replace must instead still be used.
Also, test case failures (at least for KIF tests) do not highlight in the IDE the specific line of failure. Objective-C test cases highlight with a red x the specific test line that fails. KIF tests written in Swift do not currently do this. One must instead tap “Jump to Report” and get the failure message to deduce where the test has failed.
At this point, we are willing to overlook the above snags, and move forward with adding Swift to the production codebase on a piecemeal basis. It is important to keep in mind the ground rules of Swift/Objective-C interop to devise an appropriate migration strategy:
- One can subclass an Objective-C class with a Swift class, but not vice-versa. So, converting classes over to Swift should always start with the leaf nodes rather than with commonly-used base classes.
- Calling into an Objective-C method, or using an Objective-C type from Swift, is going to go much more smoothly if the new nonnull and nullable property attributes are specified in the Objective-C signature. These differentiate whether the item is shown in Swift as being Optional or not. Absence of such a property means that Swift treats the type as an implicitly unwrapped optional -> meaning that while the variable may be allowed to be nil in Objective-C, in practice it is not supposed to be nil at the time it is called from Swift code.
- Objective-C method signatures and property types involving collection classes should first be migrated, when possible, to include support for generics. For example, rather than declaring NSArray *, instead declare NSArray <MyObjectType *>*, so that appropriate type checking in the collection can be done at compile time, and to avoid casts from AnyObject within the Swift code. Any new Objective-C code going into the codebase should adopt generics and nullability property attributes; there is no excuse not to do so.
- Swift/Objective-C interop, along with nullable property attributes and generics, work well back to iOS 7. No need to drop support for iOS 7 to work with Swift.
It also is an excellent idea to put in place a Swift coding style guide for the team, before spending too much time converting code over. Some good ones have already been written and can easily be adopted for the team.
So, when converting live app source code over, a good place to start is to pick a ViewController, or set of classes, that aren’t subclassed elsewhere in the app, and convert just them to Swift. Any hooks into Objective-C code from those classes should also be cleaned up to include nullability property attributes and generics on the collection classes.
Ideally the set of classes is within the same functional area of the app. TripAdvisor’s native app makes heavy use of feature gating. Feature gating is the ability to configure, from an API call to our server, whether or not some specific piece of functionality shows up for users. Suppose that we wish to convert the photo viewer over to Swift. A good strategy might be to leave in both the Objective-C and Swift implementations of the photo viewer in the app, for at least one app release. Then, if we see excessive app crashes from the new Swift code, we can fall back to the old Objective-C implementation without waiting for approval of the next app release. After the next release, the Objective-C code paths can be cleaned up.
By picking initial conversion candidates that are isolated to a specific functional area of the app, it becomes far simpler to build in this sort of feature gating, to minimize the risk of app downtime caused by Swift conversion issues. Once a few of these conversions have gone in, we will have a good set of Swift patterns in place to copy elsewhere in the app, and can start being more aggressive about converting more shared code areas over.
Overall, TripAdvisor is optimistic on the future of Swift in our native iOS app codebase, and has a plan in place to make the migration a smooth one, rather than a risky all-at-once rewrite.
Update: This article has been updated to reflect that method swizzling is possible from Swift under a limited set of circumstances, when the method being swizzled has the dynamic attribute or when the class being swizzled is an Objective-C class.