Real World Swift

By @cdntr on Wed 07 January 2015

Recently we launched a new Swift-based app, which was then prominently featured by Apple. It has seen significant numbers of users. In this article, we'd like to share our experiences, give our general thoughts on the new programming language, and point out some nice things Swift provides that we can use to make our apps more solid.

This is not a Swift tutorial. This post is intended for developers who don't have too much experience with Swift and are wondering how it holds up in a real-world context. We'll refer to some technical concepts and provide links to existing tutorials and documentation where appropriate.

For context, we'll first briefly explain what this new app does and what the main goals were.

A New App

You might be familiar with our main app, Duolingo, a popular language learning app with over 60 million users (Dec 2014), which Apple chose as App of the Year 2013. If you want to learn a new language, Duolingo is the go-to way to do so on your iPhone or iPad.

In addition, we launched Duolingo Test Center, which allows you to certify your knowledge of a language. This is useful, for example, if you're a foreigner who wants to apply for a new job or enroll at a university in the US or England, as these oftentimes require some official certificate that verifies your fluency. Test Center users receive an adaptive test to ascertain their language level. To prevent cheating, tests are monitored by real people.

Upon launch, Test Center for iOS was featured by Apple in their "Best New Apps" category in over 50 countries.

App Screenshot: Take Picture App Screenshot: Speak Sentence App Screenshot: Select Real Words

Goals

From a performance standpoint, Test Center is not a very performance-sensitive app. It presents mostly static content with a few controls. In addition, video is being recorded during tests to prevent cheating, but that's about it. We did not notice any performance issues with Swift, but we also did not have to look.

Much more important to us was stability and robustness of the app. Since exams take about 20 minutes and will eventually be paid, crashing during an exam would constitute a pretty bad user experience.1 Furthermore, once a test starts, you have to finish it (i.e. you can't pause it or leave the app; we do this to prevent cheating). Accordingly, we want to keep crashes to a minimum.

General Thoughts on Swift

When Swift was released, many people looked at the syntax of the language and drew comparisons and conclusions. Some said that now that they "don't have to put up with Objective-C's syntax anymore", they could jump into iOS development. Quite frankly, that seems like the wrong way to look at it. Who cares about syntax (as long as it's somewhat reasonable)? There are much more important aspects to a language, such as allowing you to express your concerns easily, and not encouraging bad behavior.

Swift can get us further down the path of enlightenment than Objective-C and some other languages might. It seems like a step forward, language-wise. If you follow some of its authors, you can tell that they've borrowed good concepts from different places, including the functional programming world. They also did away with existing (but less-than-ideal) concepts where they saw fit.

Swift was a nice and welcome step forward for us since we were used to programming in Objective-C. If your native language is Haskell (or similar), you might feel like there's still room for improvement. We're excited to see what improvements upcoming versions of the language will bring.

The Good

Swift supports some features developers have grown used to in other programming languages, such as custom operators and function overloading. Value types (types with value semantics, such as Swift's structs) can make it easier to reason about code.

We also really like its stronger, static type system which, with type inference, is pleasant to use. Generics were also notably missing from Objective-C. Finally we can have typesafe collections instead of NSArrays containing hopefully this type of object.

Let's look at some aspects that we've found really useful in more detail.

No Exceptions

So far, Swift does not have exception handling. Whether the Swift authors left them out on purpose when they designed the language, or whether they just didn't have the time, we don't know. Not having them, however, we think is a good idea, as (unchecked) exceptions make code more difficult to reason about (Checked exceptions can make it more explicit where an exception might occur, but they also tend to be cumbersome to use, and Objective-C does not support those anyway).

In fact, our seventh most common crash is due to an Apple-provided method throwing an exception (-[AVAssetWriterInputHelper markAsFinished]). That method is neither marked nor documented as throwing exceptions, so we had no idea this could happen until we saw the crash report, at which point our app had already crashed for users.

Experienced Cocoa developers will note that even though Objective-C provides an exception throwing and handling mechanism, exceptions are typically to be used in exceptional circumstances only, which usually refers to non-recoverable situations (even though there are …counterexamples… to this). In this case, the proper solution is probably not to @catch the exception, but to write better code so that it doesn't get thrown in the first place. One might argue that in this case, the exception is really more like a failing assert. But if that's the only intended usage of the concept, why keep it in a new language that has assert() and fatalError()?

In general, we want to avoid possibly forgetting to handle a failure case. Ideally, we want to catch all those problems at compile time, not when our app is out in the field. Exceptions make this hard. So what else can we use in Swift to express failability?

Optional<T>

One of the concepts Swift relies on quite heavily is that of Optionals (you might know this as the Maybe type coming from Haskell). From Apple's docs:

The type Optional<T> is an enumeration with two cases, None and Some(T), which are used to represent values that may or may not be present. Any type can be explicitly declared to be (or implicitly converted to) an optional type.

Swift provides syntactic sugar to make working with optionals nice and easy, such as nil for the None case, special unwrapping syntax, operators, and so forth. Additionally, optional chaining allows for clear and concise code involving multiple dependent optionals to be written.

So how do we use this? Optionals are a great way to encode the possible absence of a value. You can also use it to express that a function might not return the expected result (if you're not interested exactly as to why).

Why is this better than setting a pointer to nil in Objective-C? It's better because the compiler enforces (at compile-time) that we're talking about the right kind of type. In other words, a non-optional value in Swift can never be nil. Also, Swift's Optionals are more versatile in that they work on more than just pointer types.

Here's just one example of where this is useful: In Objective-C, any method that returns a pointer type, such as object initializers (e.g. -init) may legally return nil (e.g. if an object can't be initialized). An obvious example would be + (UIImage *)imageNamed:(NSString *)name;. You can't just look at that method's type and be certain that it will never return nil.

In Swift, you can be. Apple introduced the concept of failable initializers, which are quite handy to express just this on the type level. In Swift, that same example reads: init?(named name: String) -> UIImage. Note the ?, which expresses that init here might return nil if no resource named name can be found.

We use all of this a lot, where appropriate (we're trying to steer clear of implicitly unwrapped optionals and force-unwrapping). If an expression may yield nil (e.g. through failure) and we don't need to know why, Optionals are a great way to do that.

Result<T>

If you have a call that might fail and you do want to know why if it does, then Result (an Either subtype, for our functional programming friends) is a very simple but incredibly nice type that Swift allows you to define.

Similar to Optionals, Result allows you to express – on the type level – that something might be either a value of a given type, or an NSError.

Like Optional<T>, Result<T> is simply an enum with two cases such as Success(T) and Failure(NSError). The success case carries the value you're interested in as its payload. Unless there was an error, in which case you get a .Failure with a descriptive NSError.

Unlike Optional, Result is not part of the Swift standard library, i.e. you have to define it yourself. (Currently, there's some missing compiler features that you'll have to work around.)

We use Result heavily in our networking, I/O, and parsing code, and it easily beats the old NSError inout pointer pattern, or having completion blocks with both success values and error pointers (or the even more convoluted combination of success boolean and NSError pointer).

Result is elegant and can make your code better, more concise, and safer. In our app, any expression that can (non-fatally) fail returns either an Optional or a Result.

Interoperability with Objective-C

One of the key design concerns with Swift was Objective-C interoperability. It's just not feasible for Apple to introduce a new programming language and replace their entire stack of libraries with Swift reimplementations – at least not at the same time. Additionally, the development community sits on a massive amount of Objective-C code. Without decent Objective-C interop, Swift probably would have been a non-starter for pragmatic reasons.

Fortunately, interoperability with Objective-C is quite easy in either direction, and to the small extent we've used it, it's worked well for us. It should be noted, however, that some Swift concepts (such as enums) are not directly available to Objective-C.

For example, one component of our app renders PDF assets, which we wrote in Swift. We wanted to also use this module in our main app from Objective-C. Alas, some of the methods used Swift-only features, which meant that those methods were not automatically bridged. To work around that, we simply introduced a wrapper method that could be expressed in Objective-C.2

It was easy to lift some of the existing Objective-C components of our main app and use them from Swift. For that, you simply isolate the component from the rest of the app (ideally it already is modular), and then you import it into Swift via a bridging header.

The Bad

While Swift constitutes a clear improvement over Objective-C, there is still room for enhancements. For example, this newly minted language lacks some of the expressiveness found in other languages with modern type systems. But being a young language, perhaps this will change soon.

Apple pledged to keep your submitted binaries alive, but said that they might change the language as they see fit (in fact, they've already done that a few times). That means that you might have to fix your code after upgrading the compiler, or it might no longer compile. In practice, we knew this would happen and accepted it. Fortunately, we didn't have to spend too much time on "fixing" existing, previously working code so far.

Our biggest gripe – and source of frustration – with Swift is probably not the language itself, but the tooling around it. Xcode (Apple's Objective-C and Swift IDE) does not yet feel solid with Swift code. During development of our app, the IDE would often slow down noticeably or crash. There was no (or very slow) code completion most of the time, basically no debugger, unstable and unreliable syntax highlighting, a slow text editor (once the project reached a certain size), and no refactoring tools.

Additionally, compiler errors are often incomprehensible and there are still quite a few compiler bugs and missing features (e.g. type inference sometimes goes awry).

While Xcode has gotten significantly better since we started, most of those points still hold true today, and somewhat spoil the experience. We're hoping that Apple shifts some of its focus to improving the developer tools.

The Numbers

Apple announced Swift in June 2014 at WWDC. In late July of the same year, we started working on Test Center as our first Swift-only iOS app, which we subsequently launched mid-November. The development leading up to version 1.0 of the app took a bit over three months (one developer; Android and Web versions of the app were already live, so we already had a fully functional backend and designs in place).

As we mentioned before, robustness and stability were important to us, so let's see how we did on that front.

Crashes

At the time of writing, Test Center has been live for about two and a half months, and has seen a significant amount of downloads and users (e.g. due to being featured by Apple).

As with any first version, never-seen-before problems surfaced, but fortunately it seems we did not overlook any critical bugs. To date, Test Center has a crash rate of about ~0.2%, which seems decent. 3

Let's look a bit more at the crash groups (crashes that have the same cause): The number one crash group (at ~30% of all crashes) comes from an external Objective-C library. In fact, four of the top five crash groups come from Objective-C land (the fifth being a failing assert, which we leave enabled in production builds).

Also of note, the seventh most common crash comes from the previously mentioned Apple-provided Objective-C function that will sometimes @throw exceptions without being documented as such (-[AVAssetWriterInputHelper markAsFinished]).

We attribute this low number of crashes to a solid software architecture and our adherence to sound engineering principles. However, Swift eased the process of building this architecture and following best practices by eliminating entire classes of bugs by design. Correctly using Swift's type system, for example, caught and prevented numerous type errors at compile time during development, rather than ever surfacing in production.4

Compiler Performance

We got asked how the compiler performs with a project of our size. According to sloc, our project currently has 10634 lines of actual code (not counting empty lines, comments, etc).

Nuking Xcode's caches and running time xcodebuild -configuration Release takes about two minutes. A debug build takes about 30 seconds to compile. All measurements were performed on a mid 2013 Retina Macbook Pro. Note though that some of that time is spent compiling xib files, and not just Swift.5

You can certainly tell that some of Xcode's features become slower and slower as your project grows, and we are not the only ones noticing. Iteration time (the time it takes after making a code change, hitting CMD+R, and having your app run in the simulator) is also worse than in Objective-C. A quick test shows that adding a single line brings the wait up to 14 seconds, and this varies greatly depending on what you do. Making a similar change in our Objective-C based app takes about two to three seconds.

Now, obviously this is not a sophisticated compiler benchmark, so take these numbers with a big grain of salt. Hopefully you can at least get an idea of where the performance is currently at, though.

Conclusion

To long-time Objective-C developers – especially those with an interest in modern programming languages – Swift is a very welcome and exciting step forward. At the same time, it can be frustrating at times due to the (current) state of the developer tools.

We have shown that (at least for our type of application) Swift can be used to write stable, robust, and high-volume production apps. Our main app, Duolingo, already uses some Swift code, and we're planning on using it more and more in the future.

So why would you choose Swift? As long as you have up-to-date users (you must target iOS 7 or higher) and patience while developing large projects, Swift offers a fresh, well-structured language to develop with. We’d certainly recommend taking it for a spin, especially to understand the programming philosophies that Apple seems to be pushing for.

If you know what you're doing in Objective-C, then the switch to "just using" Swift should be fairly easy and straightforward. You can write apps in pretty much the same way as you would in Objective-C. Where things get interesting is when you adopt some of the more advanced concepts. In particular, there seems to be a trend of embracing a more functional style of programming, and we think that's great.

If you already have an app in Objective-C, you probably don't want to rewrite it from scratch just for the sake of using Swift, but you could consider adding new components in Swift.

If we could go back in time and had to rewrite the app, would we use Swift again? Yes.


  1. You actually get to retake a test for free if there's a legitimate crash during a test. 

  2. There may very well be better ways to do this. We did not want to lose the expressiveness of the Swift version of the method, so we opted for a simple wrapper. We might look into how to improve this. 

  3. This is the ratio of crashes to downloads. We don't have a good "per app start" metric, and the app has been started more often than it has been downloaded, so that percentage could be even lower. 

  4. I have long since been hoping for a better (replacement of) Objective-C and am a big fan of Haskell and static typing, for example. Swift is a step in that direction. 

  5. If you asked me what single thing I would like to change about this project, then it's probably dropping Interface Builder. I'm slowly replacing the existing xibs with layout code for a variety of reasons.