It was July 2014 when we organized (what at the time was most likely the first in the world) Swift hackathon here at Base, gathering over a hundred people from all over the world for a two days with full focus on new features of iOS 8, and, most importantly, Swift language 1.0 developer preview. We were both excited and scared at the same time. Swift looked cool, but using it would have meant we needed to move out of our Objective-C comfort zone, which, albeit annoying at times, we got used to and embraced as an efficient tool. Swift was nowhere near production-ready at that time, and this caused us to skip even attempting to use it in Base app for some time. We limited its usage to some occasional helper scripts while waiting for it to mature.
Almost 2 years later, in June 2016, Swift 3.0 was announced. It was a huge step forward in terms of API quality and robustness, as well as stability of the IDE (although, as we all know, there will always be room for improvement here). This is when we decided to make the first step towards using Swift in Base. However, as you can imagine, it’s not so straightforward if your app has been in development for the last 5 years, if your codebase grew to about 300k lines of code and the data model consists of over 100 entities, etc. We needed to be careful. Here comes the story of how it happened.
Note that it’s not meant to be an exhaustive migration how-to, but rather a summary of our experience of introducing Swift to a big, mature project.
Speaking of execution, our goals were the following:
- to prepare the codebase for adding new Swift files
- to rewrite a piece of existing code in Swift to prove that it works and spot as many issues with combining Swift and Objective-C as possible
- as a consequence, to enable (and encourage) our developers to write in Swift
Once we had the project configuration sorted out, we took a simple Objective-C class and translated it to Swift, just to see that it really works. Once we were done and our new shiny Swift code compiled and ran fine we considered two paths from there:
- continue rewriting simple classes, helpers, categories etc.
- do some real useful work
Rewriting categories and simple classes, like nib-based views with no internal logic or non-persistent data models, would actually lead us nowhere, given that the aim of this project was to actually learn from it – not just bump the Swift code line count. We were certainly not after the „look, we have that much Swift code (all is just categories and IBOutlet definitions, but who cares)!” approach.
I decided to fully rewrite one of Base features. I wanted it to be a significant part of the app, but also as self-contained as possible. This way in a finite amount of time I could showcase something to people saying „see this? it’s now coded in Swift”. I chose to work on Base Reports, because:
- I wrote it originally so I knew the detailed implementation and I could roughly estimate the amount of work upfront
- it’s a nice part of the app, with lots of custom views and eye-candy transitions
- it’s well separated from the rest of the app, e.g. it barely uses the database, and only for reading; also, no other feature in the application makes direct use of Reports – and that would surely simplify testing.
The execution, a.k.a. things to keep in mind
Obviously, things are not as simple as they seem, and while working towards the goal I stumbled upon situations that caused the project to take longer than expected. Here comes the list.
You can’t subclass Swift class in Objective-C
That’s pretty much it. It’s not supported. So to sum up:
- you can use Objective-C classes in Swift files,
- you can use Swift classes in Objective-C code,
- you can create Swift subclasses of Objective-C classes,
- that’s it.
Since it’s not possible to subclass a Swift class in Objective-C (even if it inherits NSObject), rewriting big class hierarchies is a bit inconvenient. Not a big deal, one may say. Fine, if you have 10 classes that share a common superclass, you could just start with the subclasses one by one and once you’re done you’d then rewrite the superclass. This works, but many times while rewriting Objective-C to Swift you want to do things differently.
You want to make use of Swift’s built-in features, like „smart” enums, structs, optionals, etc. In cases when you decide to keep the Objective-C superclass around, you would basically need to do the rewrite in two passes: first rewrite subclasses + superclass (in that order), then clean up and optimize the implementation.
An alternative approach is to start with the superclass and follow down to subclasses, but in this case your code won’t compile until you rewrite the whole hierarchy (cause Objective-C subclasses won’t compile with Swift superclass). You might obviously try temporarily excluding Objective-C subclasses from the build or commenting them out but this is not always easily doable.
It’s your decision which path to take, and apparently there’s no one proper solution. I preferred going top-down from the superclass. This way it’s also a bit more challenging to keep track of the changes you make, but given I was familiar with the implementation I was able to cope with it.
You will love (and badly need) nullability specifiers
The nonnull and nullable keywords have been there for a while in Objective-C syntax but I get the feeling that people didn’t really care about them. Or, more precisely, it was nice to stick to them, they have been a great addition towards self-documenting code for sure, but no penalty points if you didn’t use them in your code.
It changes with Swift, where nullability specifiers suddenly come into play. When Objective-C API is used in Swift, arguments marked with nullable become optionals, and nonnull become regular, non-optional values. Any API arguments without nullability specifiers are interpreted as implicitly unwrapped optionals (those ending with an exclamation mark). You don’t want those in your code most of the time, so you shouldn’t introduce such Objective-C API to Swift without previously decorating it with nullable and nonnull. While this is a fairly easy task when you authored and maintained the Objective-C class in question, in a big project you can always stumble upon a file that you’re not that afamiliar with.
Some widespread Objective-C patterns are just unavailable in Swift. Example: imagine an Objective-C protocol with a method that returns a class object, in order for the caller to initialize an instance of a given class later on. You won’t replicate this in Swift because it needs to know the type being initialized at compile time.
You should come up with some different pattern. Maybe the object can be just instantiated and returned in the protocol method to the caller? If not, then maybe you should try and make it possible? Luckily enough, the first solution worked for me in all my cases.
Be careful with String <=> Class conversion
In other words, if you played with string representation of class names in Objective-C, you might need to revisit the logic if you port it to Swift, or if it starts using Swift classes. Keep in mind that Swift returns a fully qualified class name (i.e. prefixed with module name) from NSStringFromClass. This means that in our Base app, the call to NSStringFromClass(ChartViewController.self) will return „Base.ChartViewController” (because the class belongs to „Base” module). In Base we actually play with class names a little bit here and there. That’s how I got to know about this issue, after all.
As a consequence, be careful with NSKeyedArchiver
This was a bit tricky. Base users are able to filter reports by e.g. date range and tags. Filters selection is persisted between app sessions and upgrades, but it’s so lightweight that we decided that storing it in database would be an overkill, so we just archived the selection to the standalone file using NSKeyedArchiver and conforming to NSCoding in filters classes. It always worked flawlessly, but suddenly it stopped retrieving filters in one specific case: when you installed the version using Swift on top of the AppStore version of Base.
After making double sure the archive is in place, the code is still being executed, and basically ruling out all the common mistakes, I took another look at the SDK documentation. It turned out that NSKeyedArchiver stores class name as well as the data encoded by -encodeWithCoder:. Because the class name changed (it now contained the Swift module name), the unarchiver had no chance to match the names and then unarchiving failed. Fixing this was quite simple though, with a single line of code:
NSKeyedUnarchiver.setClass(ReportsDateFilter.self, forClassName: „FSReportsDateFilter")
This means roughly „for every archive with class name FSReportsDateFilter, try initializing ReportsDateFilter class when unarchiving” (note: we dropped our poor, two-letter „FS” prefix for Swift classes).
Speaking of archiving…
So as you know Swift bridges primitive number types to NSNumber and back. Most of the time. Precisely, this is not true for Int64. It’s useful to remember once something stops working for your Int64-based data. In our case, in the filters I mentioned above, we stored database IDs of objects (e.g. tags) the reports data was filtered by. Because Swift doesn’t automatically bridge Int64 to NSNumber, NSKeyedArchiver couldn’t archive an array of Int64 numbers. The solution was to first manually convert it to array of NSNumber objects, and then pass to the coder.
We managed to rewrite the whole Reports module in Swift in less than a month with 1 person. This is 78 Swift classes, and the total number of lines of Swift code was, actually, over 9000 :) The QA phase went surprisingly smoothly for such a big number of changes, and soon the Swift-powered app hit the AppStore. What’s the big deal? The big deal is the following:
- We noticed no new crashes originating in Reports. Obviously, this is mainly thanks to the bug-free implementation and proper QA process, but one must realize that thanks to Swift’s static typing you won’t cause another unrecognized selector type of crash ever again. All types are correct (as long as the implementation is right) and all values not expected to be nil are indeed non-nil. This has a little downside though, i.e. you must ensure proper error handling. Swift code is full of type and value checks, but don’t forget to ensure graceful fallbacks where you fail to decode data, get a nil value or an unsupported type. Otherwise you might end up in a situation where users experience neither a crash, nor a working feature.
- In fact, any possible crashes in Swift code were caused by using implicitly unwrapped optionals or interacting with Objective-C (which usually means …using implicitly unwrapped optionals).
- Since introduction of Swift in Base we added a rule in a team that developers are free to choose between Swift and Objective-C for the new code they’re writing. Coding in Swift in a mostly Objective-C application requires some additional work from time to time (the classic „this new API I just did in Swift is so nice and clean, but these Objective-C classes will need to use it – let’s rewrite them in Swift too”), but in the end it seems to be worth the effort (obviously provided that your API is, as a matter of fact, nice and clean).
- In the end, developers’ morale got a significant boost. Those advocating for Swift in Base finally got what they wanted, and those hesitant to try Swift got greatly encouraged to do so. We started talking about Swift and discuss coding practices as a part of our work, whereas while coding purely in Objective-C it was more of a hobby topic.
- Any issues? There are always some, in this case just general problems with Swift, like limitations as to making API private as well as no easy way of stubbing objects, which makes unit testing a bit tricky. The development is slower at the beginning of your Swift adventure, that’s for sure. But it’s true for every new language you’re not familiar with, and one should definitely not consider it a drawback. From our experience I can say that the overhead of becoming fluent in Swift doesn’t impact projects schedule in any significant way.
- At the time of writing this article, in almost half a year since adding the first Swift code to Base, we shipped several new features written in Swift and reimplemented some of the existing ones (most notably Base Voice, that now has sleek CallKit integration!). We’re now sure that Swift was the right way to go, we’re glad it finally happened, and we’re moving on, looking forward to Swift 4.