Building Lucidchart’s iPad App with Swift

Lucidchart for iPad

In August 2014, Lucid Software brought on a dedicated engineering team — including yours truly — to improve our users’ mobile app experience. After deciding to re-write the existing Lucidchart app natively (previously a Titanium app), my team members and I decided to use Swift instead of Objective-C.

A complete rewrite was no small feat for 3 developers, as Lucidchart is a fairly large, complex app. It has its own remotely-synced file system, advanced sharing functionality, and depends heavily on JSON server APIs. Four months and 8,200 lines of Swift later, we’ve emerged bruised, battered, and optimistic about Swift. We’d like to highlight the pros and cons of our experience building a non-trivial app in Swift.

Swift, as a language, is quite nice

Swift’s new language features combine for a more expressive language. Optionals are nice in data modeling; programmers unfamiliar with the source code will never have to wonder, Can a document’s templateID be nil? Enums force you to make decisions about mutually exclusive states, for better and worse, such as when handling success or error network request responses. Much has been written about each of these so we won’t go into detail on the basics of these features here.

To give an example, a convenient feature for us was pattern matching. The categorization of documents and folders in the Lucidchart filesystem representation depends on a number of factors. Does it have a folder entry, no parent folder, and hasn’t been trashed? It goes in My Documents. Does a folder entry have no associated document? Then it’s a folder. Swift’s pattern matching on tuples made reasoning about this inherent complexity much simpler:


// This tuple describes all need-to-know information in order to categorize
// documents and folders into their correct places
typealias DocumentConditions = (
    folderEntry: FolderEntry?,
    document: Document?,
    isTrashed: NSDate?,
    parentID: NSURL?,
    creatorID: NSURL?,
    userID: NSURL?
)

// We have various collections of folder entries, documents, or both, that we collect
// into an array of tuples
var tuples: [DocumentConditions] = []

tuples += entriesWithNoDocument.map { entry in
    (
        folderEntry: entry,
        document: nil,
        isTrashed: entry.deleted,
        parentID: entry.parent?.resourceURI,
        creatorID: nil,
        userID: entry.user.resourceURI
    )
}

// ...collect more tuples

// Loop over each tuple and categorize it
for tuple in tuples {
    switch tuple {

    // Shared Documents
    case (
        folderEntry: .None,
        document: .Some(let document),
        isTrashed: .None,
        parentID: _,
        creatorID: .Some(let creatorID),
        userID: _
    ) where creatorID != self.currentUser.URI:
        sharedDocuments.append(document)

    // ...other cases for other categories
    }
}

The code reads the same way you think of it conceptually. If a document has no folder entry, isn’t trashed, and it has a creator that is not you, add it to the list of shared documents.

Please note that we plan to write a few more blog posts about useful Swift features.

Using Objective-C from Swift isn’t terrible

Apple has put a lot of work into making Objective-C classes and objects useable from Swift, and it mostly pays off. It’s not perfect, but it works well enough, even with 3rd-party libraries. You’re allowed to use nearly all of Swift’s finer features while also utilizing Objective-C objects where necessary. Objective-C initializers have all been converted to Swift, which means ambiguity around using the various Objective-C constructors ([NSArray new], [[NSArray alloc] init] and [NSArray array]) is unified under a single constructor style:

Objective-C:


UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];

Swift:


let view = UIView(frame: CGRectMake(0, 0, 200, 200))

Cocoa APIs have also been audited to let you know when it’s okay to pass nil to something. For example, when calling animateWithDuration on UIView, you can see the animations block is required but you can pass nil as the completion block, indicated by the ?.

animateWithDuration auto-complete

Using Swift from Objective-C, however, is not so nice. It requires various @objc source annotations, and many of Swift’s finest features have to be discarded because Objective-C has no equivalent. Because we started from scratch, we didn’t have to use any Swift from Objective-C. I’m not sure we would have continued with Swift at all if we had a pre-existing Objective-C codebase.

The toolchain is a pain in the neck

The biggest frustrations with Swift arise from the tooling around it. Swift 1.1 still feels beta quality, even months after its release. Luckily, Swift 1.2 recently entered beta and will address most of these concerns, so the following points should be taken as a retrospective.

1. Error messages are frustrating.

Note: Fixed in Swift 1.2

Error messages often appear in the wrong place and are generally misleading. Here’s the definition of the then method in a Promise library:


func then<U>(body: T -> U) -> Promise<U>

The body parameter is a block that takes in a T (which will be Int in this case) and returns a U. U can be whatever you want it to be. Because body is a block, you’re allowed to use the trailing closure syntax, which allows the closure to be specified outside the parentheses of the rest of the parameters. Leaving the closure blank, you see this:

Confusing error message.

You might assume this means you’ve messed up the number of arguments you are passing to your block, but double-checking the signature shows it takes just a single parameter. So you just stare at it for a while. What could be wrong? Maybe if I print?

Printing works

Ah, now it’s satisfied. But why? Maybe because I called a function that returns Void, and it inferred that as the return type? What if I return Void directly?

Returning Void does not work

Nope, that can’t be it. Let’s add a second print:

Printing twice doesn't work

Nope. I still don’t know exactly what’s going on here, but it’s something to do with ambiguity in the type inference of the return type and implicit return of single-line closures. If you’re explicit about the return type, then the compiler is much happier:

Specifying the return type fixes the problem.

This kind of thing doesn’t happen terribly often, but when it does, it can leave you scratching your head for hours.

2. Random crashes when compiling in release mode.

In release mode, the optimization flag is set to -O instead of -Onone, causing various optimizations to kick in that wouldn’t otherwise, and suddenly a type cast will crash where it never crashed before. Here’s one:

LLDB showing incorrect values when inspecting Swift variables.

You would think stepping through it with the debugger would be enlightening, but it only makes things worse because the debugger will often report incorrect values. Inspecting the variables shows that this object’s type is set to Number, not String, so how did it even trigger this case? Is it actually a Number and not a String? Is the debugger showing you the wrong line of code? I’m still not sure. Speaking of the debugger…

3. The debugger is broken

There’s nothing that makes you question your sanity or consider alternate career paths, such as giving sponge baths to the elderly, like a buggy debugger. Breakpoints will often stop on the wrong line of code (especially frustrating when it’s stopping in an else when it’s actually in an if), or in the wrong iteration of a loop. You tell it to stop at i = 3 and it stops a i = 5. Printing variables with po will often say a variable doesn’t exist or isn’t in scope, segfault, or just plain crash Xcode. This is a very common occurence when trying to print values:


(lldb) po folder.type
error: <EXPR>:1:1: error: non-nominal type '$__lldb_context' cannot be extended
extension $__lldb_context {
^
<EXPR>:11:5: error: 'DocumentsViewController' does not have a member named '$__lldb_wrapped_expr_35'
$__lldb_injected_self.$__lldb_wrapped_expr_35(
^ ~~~~~~~~~~~~~~~~~~~~~~~

Below, we’re using a login helper that does nothing different from the many other tests that use it. However, it segfaults when trying to access the `user` object. The debugger says everything is fine:

LLDB showing incorrect values when inspecting Swift variables.

4. Compilation times become noticeably slow on large projects

Note: Fixed in Swift 1.2

8.8k lines of code isn’t that much, but compiling our app in debug takes at least 5 – 10 seconds. In release mode, it’s easily 10 times that. This is mostly due to the fact that Apple hasn’t implemented incremental builds yet, so your entire target is built from scratch, even if you only changed one line of code in one file. This can be deadly to quick iterations on UI code, or when debugging issues that only happen in release mode.

5. Slow performance in some cases

Note: Better in Swift 1.2

Swift is fast in microbenchmarks, but there are common scenarios when it becomes terribly slow, even when compiled in release mode. In most cases it’s not slow enough to be noticeable to users, but our app had a bit of JSON parsing that was taking 14 seconds to parse 1200 JSON objects. This had to happen on app load for some users. Can you imagine waiting 14 seconds for an app to load? For comparison, the equivalent Objective-C code does this in well under a second. We were able to whittle it down to about 1 second, but in the process had to change all our parsing code to delay the majority of the parsing until after the app loaded. We also had to use Objective-C data structures where possible instead of Swift Array/Dictionary objects.

Conclusion

Developing an app in Swift feels very much like a game of “Hurry up and wait.” At first it seems so nice that the language syntax is clean and expressive, and you’ll be buzzing right along until you run into a show-stopping problem that could take hours to fix. We don’t regret doing Swift—we feel confident our gripes will be better 3 – 6 months from now. Swift 1.2 looks to be a breath of fresh air when it’s released in a few months. But in the meantime, make sure you have good tests and a QA team to test your Swift app thoroughly.

Be sure to give Lucidchart for iPad a try and let us know what you think!

2 Comments

  1. Hi Parker,

    Thank you for trying swift. Apple recently released Swift 1.2 (with Xcode 6.3) and I hope that you would have a better experience with this release. I was wondering if you had a chance to try the new release. More specifically, I am interested in knowing if the new release fixed the compiler optimizer bugs that you reported.

    Thanks,
    Nadav

  2. Parker WightmanMarch 4, 2015 at 11:16 am

    Hi Nadav,

    I’ve been using Swift 1.2 on personal projects and have had great success with it in terms of compile times and welcome syntax changes. The debugger is still unreliable, however. We haven’t tried Swift 1.2 here at work quite yet as you can’t submit apps built with Xcode 6.3 to the App Store, and Xcode 6.2 hasn’t even been released yet, so it might be awhile. Once Xcode 6.3 nears release, we plan on updating ASAP and can report back on performance improvements/regressions.

Your email address will not be published.