Our road to supporting both official iOS languages with the same code base
Since 2014, the Apple ecosystem has had two official programming languages: Objective-C and Swift. For library providers like Algolia, supporting both languages is a must—ideally from the same code base. The traditional approach is to support Swift via Objective-C. Despite the fact that it is much less frequently done the other way round, we chose to travel off the beaten path and have our entire code base written in Swift.
It has been a challenging adventure—but also a rewarding journey. Let’s see why.
Objective-C has been the main programming language for Apple platforms since the release of Mac OS X. It was actually imported along with NeXTSTEP during Steve Jobs’ spectacular comeback. Before that, the official programming languages for Mac OS were C/C++, and even Pascal at one time.
While Objective-C’s syntax was inspired by Smalltalk—one of the first purely object-oriented languages—its goals and implementation were more pragmatic. Objective-C is mostly a preprocessor on top of C, combined with a thin runtime. In other words, it’s an object-oriented, orthogonal extension of C. (This is why C++ can also be extended in a similar fashion, leading to the three-legged beast that is Objective-C++… but let’s keep away from horror stories for this article.)
While Objective-C has been criticized by many, mainly for its unusual syntax, it is an elegant programming language. It’s highly dynamic, making difficult software engineering problems like proxying or mocking a breeze. It has a very clean object model, with classes and metaclasses. Though not fashionable any longer, it remains a remarkable piece of software engineering.
A leap into the future
You must be wondering: if Objective-C is such a great language, why did Apple feel the need to move away from it?
Even though Objective-C has evolved over the years, adding modern constructs like closures (called “blocks” in Apple parlance) and generics (more like type annotations, actually, but let’s not be picky), it still suffers from a few shortcomings.
Its main drawback is its lack of compile-time checks. The flipside of being a dynamic language is that many things happen at runtime, making it harder for the compiler to detect potential bugs. Also, because every function call translates in a call to the runtime, performance remains intrinsically limited.
And, of course, there is that unfamiliar syntax that can deter newcomers. I actually suspect that this is the main reason why Apple moved away. Not the flaws of the language in itself, but the fear that programmers would steer away from their platforms because they didn’t feel comfortable writing code for them.
Swift: powerful but complex
So, what is Swift all about?
Swift brings a seemingly familiar, curly-brace touch to Apple programming. Looks can be deceiving, though, because it ships with many modern notions not supported by other “curly-brace languages”, like optionals, case classes, or pattern matching.
It also brings not-that-modern, but still useful notions like true generics (as opposed to C++’s templates or Java’s type-erasure system), value types (supported by C++ from the origins) or default argument values (idem).
More importantly, Swift chooses a different trade-off between robustness, speed and dynamicity. It is strongly typed—sometimes bordering on rigidity, as with optionals, but that’s for the good cause—which translates into more compile-time checks… and less runtime surprises (a.k.a “bugs”). It is fully compiled, instead of just translating into runtime calls, which means it can run faster, especially since it leverages the awesome LLVM compiler infrastructure and all the optimizations it provides.
All these benefits come with a cost, though. While its syntax looks easier, Swift is actually a difficult language to master—as complex as C++ is. Also, it uses unique constructs, like
if let or
guard else conditional assignments, and an odd error handling mechanism—a hybrid between exception throwing/catching and traditional return codes.
Therefore, its benefits for new programmers is not that obvious. I have seen junior programmers pick up Objective-C really easily and quickly, while I must admit that I still struggle with Swift at times.
Jumping headfirst into Swift: Lessons learned
At Algolia, we are always eager to try new things. It’s part of our DNA. So, when Swift 1 was released, we jumped on the opportunity—that was our first mistake.
There was no obvious need for it. Objective-C was still supported: the entire legacy code base, including Mac OS and iOS themselves, was written in it, so it would not disappear in the near future. Because of that, Apple had made sure that Swift could leverage Objective-C code, which means we could have supported Swift without writing a single line of code in that language.
However, some customers at that time were already going full Swift and asking us to follow them— the adventure looked too exciting for us to pass up.
We wrote our Swift API Client, while keeping our Objective-C API client.
A few months later, Swift 2 was released. It broke everything, but since version 1 was mostly experimental, it was fine to rewrite the library for our next major release..
Maintaining two code bases hurt. Algolia is a fast-paced company, with new features being released almost every week. Since many of those features need to be reflected in our API clients, those libraries evolve rather quickly as well. Therefore, having two very distinct code bases to support the same platforms began to itch… especially given a major refactoring was required to pave the way for our Algolia Offline release.
The logical consequence was to drop one of the code bases—easier said than done! Deciding which one to drop was tough.
The natural choice would have been to keep Objective-C. Its feature set is entirely covered by Swift (but not vice versa), and it’s more mature, with a stable API and ABI. It’s what most people advise, for good reason.
On the flip side, Swift offers nice abstractions that better fit our needs. In particular, it can make value types (like integers or floats) optional, which is useful for handling our search parameters. It also provides advanced enumerations, allowing us to deal elegantly with special cases, like our removeStopWords parameter, which accepts both a boolean or a list of strings.
Finally, as stated above, some of our customers already went 100% pure Swift, and we didn’t want to betray them.
Swift first, Objective-C as a fallback
So we chose Swift. It turned out to be a challenging task.
We didn’t do it right from the start. We had to fail, and learn from our mistakes—only after version 4 of our Swift API can we speak from a state of confidence about our decision. That said, we may still discover mistakes in the wild out there, waiting to be fixed in future releases.
Because the ecosystem still has a huge proportion of its code base written in Objective-C, Apple went to a lot of effort to “bridge” Objective-C and Swift as seamlessly as possible. Whenever some Swift construct has an equivalent Objective-C construct, it is automatically made available from Objective-C—it’s like magic.
The problems arise when the magic falls short, simply because there is no equivalent notion in Objective-C for what you are trying to express in Swift. In particular, the aforementioned optional value types or advanced enumerations cannot be bridged.
The easiest way to solve this is to use a subset of the Swift language, namely only features that can be mapped to Objective-C. It’s safe, but it negates the purpose of choosing Swift as an implementation language in the first place. If we limit ourselves to what is supported by Objective-C, then we might as well write code in Objective-C directly. Also, by using types or constructs that are not typical of Swift, we condemn ourselves to a poor developer experience (DX).
By writing some Swift-specific code and having an Objective-C compatible fallback whenever necessary, we gave ourselves more work up-front—and probably a little more maintenance—but it yields a much better DX in the end.
Let’s see how it goes in practice with our
Query class, which gathers our search parameters. Every parameter is embodied by a property with the same name. It is first implemented with the optimal type for Swift. Since most of these types are bridgeable to Objective-C, no more work is required for most of the parameters. For the few non-bridgeable properties, we add an Objective-C compatible fallback. This property is inevitably also visible in Swift, which could create confusion for the developer. However, we use a few tricks to mitigate the impact:
- The Swift name is prefixed with
z_objc_, which makes it obvious that it is intended for Objective-C, and also ensures it appears last in code completion.
- Using an
@objc(name)annotation, we map this property to the parameter’s name. Since the original property is not visible in Objective-C, no conflict is induced by this. All goes as if the property simply had a different type in Objective-C.
- The fallback property is not documented, which means it does not appear in the generated reference documentation.
We looked at other potential solutions, like having an Objective-C specific subclass. This leads to even better name insulation; however, it poses tricky covariance issues when extending the class, like what is done by InstantSearch Core for Swift. Finally, naming tricks were easier to maintain and created less friction for users of the library.
The end result is cool autocompletion that works smoothly from both languages:
Smoothing out the rough edges
We discussed properties, but what about methods? Thanks to named arguments, Swift is actually quite close to what Objective-C can achieve. And thanks to well-established naming conventions, Xcode is able to automatically compute the name of a selector from a Swift function, and this computed name will be suitable 90% of the time. For the remaining 10%, though, explicitly specifying an Objective-C selector name can lead to a better DX.
- the Swift function
batch(operations:completionHandler:)would normally translate into the Objective-C selector
batchWithOperations:completionHandler:, but we preferred
browse(from:completionHandler:)should translate into
browseFrom:completionHandler:, but we chose to make the first argument more explicit with
Generally speaking, it is OK to be more verbose in Objective-C than in Swift.
Also, code is fine, but shipping a good software library involves more than just writing code. Extensive documentation is a must.
The traditional tool used in the Apple ecosystem, Appledoc, only supports Objective-C. Instead, we use Jazzy, which supports both Swift and Objective-C, but (at the time of writing) only generates Swift documentation from Swift code. Objective-C developers have to guess the selector’s name based on the Swift function’s name—or rely on Xcode’s autocompletion. As you can see, there is no perfect solution yet for cross-language documentation.
When Apple falls on your head
The cool stuff with Apple is how predictably unpredictable they are. Just when we thought we had Swift figured out… Swift 3 was released, and broke everything. Again.
Being hit by an apple is not necessarily a bad thing (ask Isaac Newton). Swift 3 brings huge improvements over Swift 2, and was totally necessary. It really looks like “Swift finally done right”. The migration gave us an opportunity to improve our code, sometimes in a backward-incompatible way that would not have been possible without a new major version.
Still, a forced migration does have some drawbacks. You need to support two branches for a while; more importantly, you don’t control the timeline—and that’s especially true with Apple. The scope kept moving until the final release, too. Swift underwent a tremendous amount of changes between the first and the last beta versions (making them more like alpha versions). There were even changes between the last beta and the Gold Master (GM)!
Regardless of this constant stream of changes, customers asked for our support from day one. They wanted their app to be updated in the App Store as soon as iOS 10 was out. All this conspired to create a “rush effect”—which is never the best way to ship quality software.
We managed to handle it pretty well, all things considered. Version 4.0 of our Swift API Client was out on September 14th, 2016—one day after Xcode 8.0 final was officially released.
All of this could have been avoided if modules were distributed in binary form, but Cocoapods encourages delivering modules in source form, and anyway Swift doesn’t have a stable ABI yet. Although ABI stability was initially planned for version 4, due in Fall 2017 (see the Swift Evolution Proposals), it now appears that it will be deferred again.
Exploring the jungle
As a conclusion, supporting both Swift and Objective-C from the same code base is definitely possible, from either of the two languages. Which one you pick is a question of which compromises you’re willing to make. Objective-C is the safe and easy choice. Choosing Swift is more complicated, and will expose you to more hectic schedules; however, you will be rewarded with a slightly better DX for your Swift users.
Having walked this path, and being able to weigh the benefits and drawbacks, I would still recommend sticking to Objective-C for the time being as far as universal libraries are concerned. The decision for a standalone application is an entirely different tradeoff, and Swift does make a lot of sense in this case.
Despite the huge improvements Swift has undergone, the language is not yet mature enough to write future-proof software—as all good libraries should be. It is surprisingly insufficient in some crucial areas, like inter-thread synchronization. Even today, it’s not uncommon to see the compiler crash on erroneous input—instead of cleanly exiting with an error message—or burn 100% CPU for several seconds before finally giving up on evaluating a seemingly trivial expression.
Hopefully, all that will be solved in the future, making Swift a first-choice language for all types of developments. In the meantime, I’ll keep exploring the jungle for you.
NEW! InstantSearch Core for Swift: a high-level library to develop rich search experiences