Building Lucidchart's iPad App with Swift
Parker Wightman
Reading time: about 10 min
Topics:
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 ?
. 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:
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?
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?
Nope, that can't be it. Let's add a second print:
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:
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:
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:
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!
About Lucid
Lucid Software is a pioneer and leader in visual collaboration dedicated to helping teams build the future. With its products—Lucidchart, Lucidspark, and Lucidscale—teams are supported from ideation to execution and are empowered to align around a shared vision, clarify complexity, and collaborate visually, no matter where they are. Lucid is proud to serve top businesses around the world, including customers such as Google, GE, and NBC Universal, and 99% of the Fortune 500. Lucid partners with industry leaders, including Google, Atlassian, and Microsoft. Since its founding, Lucid has received numerous awards for its products, business, and workplace culture. For more information, visit lucid.co.