What do you do when you are ready to upgrade to Swift but rewriting your existing Objective-C apps is not an option? In this try!Swift talk, using Etsy as a case study, Amy discusses a blueprint for integrating Swift incrementally into your apps.
Swift provides rich features for Objective-C interoperability, but applying them to your current codebase is not always straightforward. Amy covers technical details, such as linting and managing dependencies, as well as organizational strategies for gathering support, and other things they have learned at Etsy along the way.
After reading this, you will be prepared for a smooth transition to Swift: both in your code and in your company.
Introduction (00:00)
Let’s talk about adopting Swift incrementally in your apps. What I want to cover today is using our experience from Etsy as a case study. I am going to tell you a little bit about:
- Where we began and how we decided to get started with Swift.
- The experiments we ran to start getting into our code base.
- The lessons we learned or the things that we broke along the way.
Etsy is a global marketplace for people around the world to connect online and offline to make, sell, and buy unique goods. And at Etsy, we have four native apps. We have an application for buyers, an application for sellers, and both of those are on iOS and Android.
Swift (00:35)
In 2015 when Swift 2.0 came out, and Swift went open-source, it seemed to us like it was completely magical. A lot of other engineers and I at Etsy started getting excited about how we could use Swift ourselves.
I would look at a class, and I would say, “This would be much better if I could rewrite it using generics.” Or, “If only I had compile-time API availability, then writing this feature would be much easier. And we started saying to ourselves; we could stay with Objective-C or maybe we should look at rewriting everything.
There have been wonderful articles from different companies, one from Lyft comes to mind, where they talk about how porting their app to Swift made it faster, it made it smaller, it reduced bugs. It made their developers happier. And all of these things were things that seemed magical and exciting to us, and we wanted that, too.
We said, “Great, let’s rewrite everything!” And we decided to put together a proposal to over time, incrementally port our app into Swift, class by class. And the first thing we needed to do of course was to build consensus. What surprised me is when I talked to people outside of iOS development about starting to use Swift, I had two comments from people.
People would say Swift has been out for like a year: Are you not using it already? Isn’t that something that happened? Or people would say, Are you totally crazy? Why would you rewrite your app from scratch?” gathering consensus was an important part of this process, and the way we decided to do that was through a process we call an architecture review. All that means is you write a proposal and get a bunch of smart people in a room, and the burden of proof is on you to explain why you think your new code, your new idea is going to make your lives better.
We have these smart people together, and one of them asked, “How do you know that all this Swift code you write is going to be better than the Objective-C that you already have?”
And this turns out to be a good question for us because we actually have a lot of Objective-C. As of August 2016, we had:
- 5+ years of commit history.
- 280,000 lines of Objective-C and counting.
- 2,500 implementation files (and I am excluding our libraries).
I bring this up not because it is bad. All that code represents much experience and authority and expertise with Objective-C. Throwing all of that out and starting over might not have been the most prudent approach for us. We decided we needed to find an answer to that question; we needed a reason to use Swift that was not just because it is magical and cool and exciting.
We decided to take a step back, and we said, “Maybe we do not have to live in one world or the other. Let’s just start writing our tests in Swift.” And that is exactly what we did for three or four months: we started writing functional and unit tests in Swift, using that as a vehicle to answer that question.
Stability and Strategy (03:22)
When you are moving back and forth between these two languages, you start to look at Objective-C and start to realize that it cannot express certain things. You will be writing a function, and you will look at it, and you will be, what if this argument is nil? Will everything break? Probably.
And that gave us the answer. We said, “We do not want to use Swift because it is cool. We want to use Swift because we think it is going to let us write safer code that crashes less.” And from there we were able to go and build that consensus again.
We had another architecture review with a new proposal, and we started to talk to people about our new goal, which was instead of moving everything to Swift and making Swift development mandatory, let’s just make it possible.
Because if we believe that the Swift code that we write is going to be better and safer, then we could start to write new features with Swift and keep all that old Objective-C that we already know and trust. That is what we landed on.
We said, “Instead of living with just Objective-C or Swift, let’s be a two-language code base. Let’s commit to that.” And this was actually a smart decision, I think, because realistically if we were porting our app one piece at a time, it would take us years to get completely to Swift anyways.
Disadvantages, Advantages (04:27)
There are many disadvantages to this approach. Obviously, it means you are in this awkward, in-between code base state.
Xcode forgets what language you are looking at and gives you the header for the wrong one. More importantly, developers need to know two languages, which is also a disadvantage in terms of education. And finally we have to deal with all of those messy interoperability features i.e. bridging headers and auto-generated Objective-C.
But to us, the advantages outweighed that. If we are adapting Swift piece by piece, it gives us more time to adapt, to risk. It also lets us figure out how Swift is going to work for us. And finally, it gives us time to learn Swift as an organization, so we know that by the time we write more Swift, we are going to be better Swift writers.
We needed an approach for how we were going to start using Swift at all, and the approach we landed on was Swift by experiment.
Swift By Experiment (05:09)
We said: we are going to make hypotheses about how we think adding each piece of Swift to our code base will go, and then we are going to find a way to test them.
This was important because we knew that things were going to break, and we wanted to make sure that anything we broke did not affect our users or our actual app in production. Things were going to break for many reasons, including that Swift is unstable.
It is unstable in the sense that it is under active development, and Apple is still making breaking changes; also unstable in the fact that sometimes it just crashes - Xcode crashes, playgrounds crash.
We want to make sure that does not actually cause problems for us in the wild.
We also realized that our app is a part of a larger ecosystem: we are not just a good repository, we also have third-party services. Crash logging, and we have to submit bills to Apple; we have bill machines, we have to do translations (and probably things we have not even thought about).
It was important for us to ask ourselves, each of these external things that touches our app, how is adding Swift going to affect it.
Then we came up with three goals:
- Let’s just add this Swift run-time.
- Add our first Swift class, and A/B test it
- Start developing new features in Swift.
Adding The Swift Runtime (06:29)
When you start shipping Swift in your app, because Swift is not of a stable binary interface, you actually ship all of these libraries, the dynamically linked libraries required for this Swift runtime with your app.
When you check a bunch of boxes, we decided to ship some code that does not run; we added a hidden view controller in Swift, without having users running it.
The very first important thing we learned: make sure that those libraries are actually there. If you unzip an IPA file with a Swift app, you will see a folder next to the payload called SwiftSupport. And that should be filled with a bunch of dynamic libraries like swiftlibcore.dlib.
If those libraries are not there, Apple sends you a nasty email and rejects your app. Save yourself a heartache, and unzip it. It turns out that certain headless builds, Xcode build included, do not include this folder by default. The same bug exists for WatchKit apps.
The other thing we learned is: monitoring build sizes. Those libraries in the Swift runtime add up to something in the order of 17MB (it is fairly substantial).
I wasted time worrying about, is this going to bump us over the over-the-air download limit, which would affect downloads and that would be bad. I spent much time trying to come up with scripts that would tell me how big is our app going to be when we submit it to the app store? And it turns out that is hard because of things like app thinning, and Apple compresses it. You cannot answer the question of how big your app is.
The solution is just: upload it to iTunes Connect. Deep within all of the menus, under Activity, All Builds, and you click on a build, Apple will tell you exactly how big the app they are going to ship to your users is.
After that first experiment had succeeded, we decided to move on to running our first Swift code.
Running Our First Swift Code (08:15)
This was experiment number two. The approach we decided to use was an A/B test. Line by line, we rewrote an existing simple view controller. What is useful is we were not testing our ability to write novel code in Swift, we were just testing Swift itself and how it interacts with the rest of our Objective-C code base.
This experiment also taught us something new: it crashed.
I have a pop-quiz for you. Where does this code crash?
guard let collection = self.collection else { return }
let isPrivate = collection.isPrivate()
let isFavorites = collection.type == "favorites"
It turns out it crashes on line three when you access the property collection.type
.
What was happening here is collection
was an instance of an Objective-C class, and we did not add nullability specifiers to it. And what that means is that collection.type came through as property implicitly unwrapped string. An implicitly unwrapped optional. Hence it is effectively optional value that is automatically forced unwrapped for you when you try to access it.
This was an important lesson to be learned: annotate your files. And this was interesting for us because, of course, with something like 2,500 header files, annotating all of them is completely impractical.
The solution we landed on was to annotate files while you import them. Your Swift bridging header is the firewall between your Swift code and your Objective-C, as long as you annotate things there, you will be safe.
And a special word of caution, do not forget that headers nest, a header that imports a header, you need to make sure your Swift annotations are available in all the headers that it links.
Another thing we learned is: our crash logger did not give us useful information in Swift. We were trying to figure out where our code was crashing and we received random garbage in our stack traces.
It turns out that an interesting property of Swift is, because it has proper namespaces, you did not make sure there are naming collisions in the compile on the linker level. All these symbols in Swift were compiled to this mangled format.
There is a useful tool shipped with Xcode tools called Swift-demangle. You could take your stack tracing, put it in Swift-demangle, give you a proper looking stack trace. With our crash fix, we decided we were ready to start writing new code in Swift.
Writing New Code In Swift (11:10)
We came up with a team goal: do not write any Swift that another developer would have to rewrite to use from Objective-C.
The reason I say this is because we decided that code reuse was an important goal. We did not want to end up in a situation where someone writes a new and exciting utility in Swift, and then another developer wants to use it in Objective-C half of the code, but they cannot.
This is actually harder than it sounds because many features in Swift are not backward compatible with Objective-C (Generics, Tuples, Structures). All of these things that, if you have them in your Swift code, then you are automatically generated to Objective-C header just will not include them all.
The solution we landed on was simple: use access levels. We have private in file, and private in Swift 3.0. All we decided is that if you are going to use, for example, a generic or struct, make sure it is flagged as private. You can write your very Swifty code, but it will be interfaced for it. You are forced to write something that you can still use from Objective-C.
But, how do you even force this? And the approach we landed on was: using a linter.
Linter is a small piece of software that takes source code as input and output style violations (commas and braces). You can use this to do more powerful things. I wrote a linter rule on top of the great open source project called SwiftLint.
Code/Swift/Interoperability.swift:19:2: warning: Objective-C Interoperability Violation: Object ‘someFunction(_:_:)’ of type FunctionFree should be private, but is internal (objective_c_interoperability)
Linter rule looks through and makes sure that you are not using any of these Swift-only features, in a way that is publicly accessible. If you do, it warns you about it, and you can make sure that you keep that code out of our code base.
Finally, the last thing we learned is that much of our Objective-C code looks bad in Swift. It is not very Swifty. And it turns out there is this fantastic macro called NS_REFINED_FOR_SWIFT
.
If you have a piece of ugly Objective-C code, you can tag it with this macro and then write an extension in Swift.
@interface MyClass : NSObject
- (void)anUglyFunction NS_REFINED_FOR_SWIFT;
@end
extension MyClass {
public func aPrettierFunction() -> Void {
return self._anUglyFunction()
}
}
That lets you redefine how Swift will see that function you can make it more canonically Swifty. If this is an approach that is useful to you, there was a great talk WWDC 2015 called “Improving your existing apps with Swift.”
Education, Standarization And The Future (13:22)
We wanted to make sure we were still answering that question of How do we make sure that the Swift coded writing is going to be as good as or better than the Objective-C we already had?
The very first thing we needed was code standards. We wanted to make sure we all agreed on the Swift code we would write.
It was interesting that none of us felt we had the authority to write our own Swift code standards from scratch. This was a new language to us too. What we did, and my suggestion to you is, of course, you can borrow some.
Many companies publish their Swift code standards publicly. We started with GitHub’s. You take that starting point, and we started to modify it with our own concerns about interoperability to get to the code standards that we have today. Other thing, of course, is do not forget to standardize a new version.
As soon as you have more than one Swift developer, you are probably running more than one version of Xcode. And this is not a problem in Objective-C but it quickly becomes a problem with Swift.
Because you are going to start shipping code, and then you are going to start your new wording for each other, it is not going to compile it all because you are all running slightly different versions.
From the command line, you can check what version of Swift you are running, and just agree on one. Swift.org now publishes tool chains that you can install into Xcode separately from the Xcode version. Or you could all install Xcode at the same time.
Then, of course, we have to deal with future versions of Swift. Adopting Swift 3.0 potentially has the same problems of adopting Swift in the first place. It is a breaking change. How do we deal with that? We decided to keep the same experimental approach, on an on-going base.
We looked at running Swift 3.0 in a branch using the code migrator as installing future versions of Xcode in one of our build machines. This lets us use the same experimental approach to make sure that Swift 3.0 was not going to cause any problems either.
Finally, we have education. This is something that is on-going for us. And it is something that we are excited about: “How do we get other developers outside of iOS to contribute to the iOS apps? The approach we are taking is to run a series of workshops and lunches where we start to introduce Swift to the larger population.
The Bigger Picture (15:23)
If you are sitting in the audience and you have big Objective-C, and you want to start using Swift tomorrow, what is my advice to you and where can you start?
The very first thing is you need to find your raison d’etre, your killer feature: you need a reason to use Swift. And it cannot just be FOMO. It needs to be something that is good for you as a developer or your users.
Once you find that reason, you start telling people about it, and it becomes much easier to get other people on board with you using Swift in your code.
My next suggestion to you is to start outside of your codebase. For us, starting by writing test was invaluable. One thing that is important is it let us gain experience with writing Swift. But another thing that is important is that it let us gain experience with writing Swift at Etsy and figuring out what does that mean for us.
And when you start outside your codebase, whether it is for tests or tooling, or toy projects or anything like that, you give yourself the opportunity to learn as a group.
My next suggestion to you, of course, is to make a test hypothesis; all the things that broke for us, they may break for you, you are probably going to find some new ones too. Every system is different, and every system is unique. And the only way you can figure out what is going to give you a problem is by trying it.
My suggestion is to make a diagram, figure out all the things that touch your app and ask yourself “If I had Swift, is this going to change?”, or “How is this going to change?.” And then for every one of those things, try to devise an experiment to figure out how you can actually test that without breaking your app in production.
You can do it. We are lucky to be working with these two very powerful languages that are interoperable. It gives you tools that we might not have otherwise. Do not be afraid to get started.
Thank you!
Receive news and updates from Realm straight to your inbox