Since WWDC, I’ve spent a lot of time playing with Swift.
I’ve even shipped two apps written in the language:
EventBoard IT
at work and Maidenhead Converter on my own
time.
Overall, I quite like the language, but it’s clearly in an early state. I would
like to talk about Swift in greater detail later, but for now I want to focus
on one particular issue I’ve had that I only just recently conquered, and some
surprising additional benefits that the solution provided.
Compile Times
The Swift compiler is slow. On my MacBook Pro, every 100 lines of code
adds a full second to the compile time. EventBoard IT weighs in at about 4,000
lines of code, and takes 42 seconds to compile. When the same machine can
compile a 30,000 line Objective-C app in a fraction of the time, that’s
unacceptably slow. My much faster iMac compiles EventBoard IT in a third the
time, which is better, but still too slow.
This one issue was leading me to postpone doing further significant work in the
language, which saddened me. I tried numerous suggestions to improve
compile times—explicitly type everything instead of taking advantage of
the implicit typing feature, watch the build log to see if any one file was
taking a disproportionately long time, etc—but nothing seemed to make a
difference.
While the compiler needs to speed up generally, one of the biggest
problems it has right now is that it doesn’t delta compile; every time you hit
build it will recompile the entire app, and not just the parts that changed
since the last build.
The Swift team explained that this is because of the nature of the language.
In Objective-C you have explicit #include
statements that make it really easy
to build a dependency graph, which makes it possible to see which parts of an
app might be affected by a change in one source file. Swift doesn’t have that:
any source file in a target can see any symbol in any other source file in the
same target. Because of that, the compiler can’t easily judge which files might
be affected by a change in another file. The Swift team has said they intend to
implement dependency analysis in the same way the C#
team does, but that doesn’t exist yet, so your entire app recompiles every
build.
As it turns out, there’s a way you can take advantage of delta compiles in Swift
today: embedded frameworks. While the compiler can’t do dependency analysis
inside a target, it can do it between targets. If the code in an
embedded framework didn’t change since a previous compile, it doesn’t need to be
recompiled.
A few days ago I started pushing parts of EventBoard IT out into embedded
frameworks. I’m not done with the process yet, but I’ve already cut the compile
time nearly in half: a delta compile takes 24 seconds. Interestingly, a full
compile only takes 30 seconds now. I suspect that’s because of a combinatorial
explosion effect, where every file you add to your project adds to the compile
time of every other file in your project. Since the project is being split into
different modules, each module has less to consider than the app as a whole did.
This fix makes me excited to use Swift today again. But as it turns out,
that’s not the end of the benefits that embedded frameworks yield.
Header files in languages like Objective-C have one really distinct advantage:
you can declare and document your public interface in the header file, while
hiding all of the implementation in the implementation file. When done well,
you can look at a header file to learn how to use a class,
without ever having to look at the implementation. Modern languages don’t have
header files, and so everything goes in the implementation file. This is
unfortunate, but not so unfortunate that I think header files have any place
in a modern language. Swift made the right decision ditching them.
But wouldn’t it be nice if you still had a view in Xcode that just showed you
the public interface of your code, with its documentation comments? You get this
view when you inspect symbols on any built-in framework like UIKit, but what
about your own code?
Embedded frameworks give you this public header view. In source files that use
your framework, you include an import
statement. If you command-click the
framework name in the import
statement, you will see the public header view.
It shows the documentation comments you wrote and the public symbol
declarations, but not your private nor internal symbols, and not the
implementations of your public symbols. It’s glorious.
Limited Scope
When you write a view controller, you often write various sub-controllers or
views or model objects that are used by the view controller, but not by anything
else in the app. Your classes might also have public properties and methods
that are used by these near-neighbor classes, but are not relevant to the rest
of the app. Unfortunately, even though these classes are only used by the one
view controller, their symbols are visible to the rest of the app. Which means
that anything else in your app could instantiate or manipulate them.
In my experience, one of the best ways to improve robustness in your software is
to limit the scope your code has visibility of at any given moment. Minimize
global variables, use const
wherever you can, etc. The fewer moving parts your
code has access to at any given point, the fewer moving parts it can potentially
break or depend on in brittle ways.
Classes are, in a sense, themselves global variables. NSObject
can be thought
of as a global variable that refers to an object that can be used to construct
instances of NSObject
. Every class you declare adds another
object-constructing global variable to the scope of the rest of your app. A bit
unnerving to think of it that way, no? But what can you do? Either hide all of
your view controller’s related classes in the same implementation file, or
accept that you’re exposing those symbols to the rest of your app. It’s
workable, but it kind of sucks.
Swift’s access control support combined with embedded frameworks improves this
situation. By default, symbols in a Swift framework are internal
, which means
they can be seen by anything within the target, but nothing outside it. In an
embedded framework, you have to explicitly annotate symbols with public
to
make them visible outside the target. So you can make a view controller with a
host of utility classes and only annotate the view controller itself as
public
, and then the rest of your classes have no visibility to the rest of
the app. Boom, those classes are no longer global variables—at least, not
outside their framework.
A related benefit: you’re forced to think more explicitly about exactly which
symbols should be public. You ask yourself if something really needs to be
public, or which module something really belongs in. It helps push you towards
clean interfaces.
I’m Loving Embedded Frameworks
It feels like these new embedded frameworks in iOS 8 are going to result in a
significant change in the way I build apps. I’m going to try to continually silo
things off in their own framework wherever I can. The compile time complaint
will (hopefully) go away with improvements to the compiler, but the public
header view and especially the improved ability to limit scope are really
substantial benefits.