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.
Public Header View
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.