Stupid Swift Tricks #4
Towards the end of last year, I released /products/ferrite Ferrite Recording Studio], which is a fairly large, sophisticated app written almost entirely in Swift, during a year in which Swift itself was rapidly evolving.
I hear a lot about people taking a wait-and-see approach to Swift, or dipping a toe in by migrating a few pieces here and there, or even in some cases outright rejecting it… but I haven’t heard many people talking about diving in head-first on a big, “Pro App”-sized project. So I thought I’d write up something about it.
First, the tl;dr:
I’m really liking Swift.
I would totally do it again.
Which isn’t to say that it was pain-free: there were certainly times when I doubted the sanity of diving that deeply into Swift, that early.
But it paid off: I believe I wouldn’t have been able to write an app on the scale of Ferrite, in anywhere near the time it took, using Objective-C, and the result speaks for itself: it’s probably the most powerful and stable thing I’ve written, won a Best New iOS App of 2015 from Relay FM’s Upgrade podcast, and one of their new podcasts, Canvas, is produced with it.
Writing in Swift enabled a bunch of interesting techniques I expect to be using a lot going forward
A little on where I’m coming from, just so you have some idea of my biases when it comes to programming languages: I’ve been programming for a lloooonnngg time (since the early 80s), on a variety of platforms and in a variety of languages. My favourites prior to Swift were Python and Objective-C. I’ve been programming for iOS since as soon as non-Apple folks could – I had an app in the App Store the day it opened – and so I was pretty comfortable with How Things Were Before, and have in the past leaned more towards the dynamic side of the dynamic/static debate.
So why dive into something new? Well, to be honest, kinda the same way that Jason Snell started editing podcasts on an iPad: it started off as an experiment, and just… never stopped. :P
Ferrite Recording Studio is an audio recording & editing package. If you like, you can think of it as being “like GarageBand but different”: it doesn’t have GarageBand’s music focus, musical instruments, etc. But it supports much, much longer recordings and projects, features like crossfading, strip silence, ducking, automation, audio unit extensions, and more, and a streamlined interface. All this falls out of a focus on uses like journalism, podcasting, recording lectures and speeches, and so on.
This has a number of implications:
It needs to be super reliable – Ferrite is being used as a field recorder by mainstream broadcast journalists, sometimes in stressful situations.
It needs to cope gracefully with a lot of data – many episodes of The Incomparable have been edited on Ferrite – these can include up to 8 participants and typically run in the 1-2 hour range, so an episode can potentially include 16 hours of audio.
As an audio app, hard realtime requirements mean that parts need to be written in C or C++ without locking, allocating or freeing memory.
But it’s no tip calculator: there’s a lot of features in there to implement, and to stay stable and maintainable over the long term, that complexity needs to be managed carefully.
All of this heavily impacts the design of the app, not just in terms of UI, but the underlying architecture.
There’s some wisdom to the idea that you should try just one major new thing for each project, to minimise risk. Try out dramatic changes on smaller projects, and if they work out, make use of them again.
I… uh. Didn’t do that. While I don’t recommend that, this came down to the fact that I approached it initially as a short experiment that incorporated a bunch of “radical” ideas. It turned out, they were all so successful I just kept on going and built it up into A Real App. Some of the most notable of these new things were:
Writing in Swift, obviously.
Extensive use of PaintCode to generate resolution-independent art as code.
Using a new document model very loosely inspired by (but which isn’t) Wil Shipley’s use of Git as a Document Format to ensure that projects are always kept safe and undo/redo works even when you come back to a project later.
Using a new application model very loosely inspired by (but which isn’t) Facebook’s React to keep my brain from being overwhelmed by the flow of data inside the app.
Switching to Auto Layout for almost all layout needs, with a single Storyboard for both iPad and iPhone.
All of these experiments were successful and I intend to keep going doing this road. Going through all each of the items in detail would make a monstrous article – I hope to write more about some of them in later posts. For today, I’m just going to try and give a quick overview of the first item: how Swift impacted the project.
Almost all of Ferrite is written in Swift. There are parts that aren’t: some third-party libraries to handle common, standardised tasks (most notably, the excellent YapDatabase), and some old library code of my own. The only code written specifically for Ferrite that wasn’t in Swift, is the audio playback engine.
This can’t be written in Swift or Objective-C since the audio thread has hard realtime deadlines to meet,1 and the runtimes of these languages aren’t realtime-safe. So the engine is C++ (kinda).2
Swift made Ferrite possible. I’ve created big projects before, but Ferrite is not only bigger, but more maintainable and more reliable, without taking extra resources to develop. This is despite the sometimes-quite-significant additional problems I encountered.
Why is this?
I’d put it down to a handful of things:
lowered cognitive load
lowered impedance-mismatch
non-astronautical abstraction
There’s quite a lot of overlap between them; I feel like the three sections are spiralling in on the crux of it from different angles. From a big picture point of view, it’s something like “less stuff = less problems”.
But talking of which…
Working in Swift was far from flawless.
It can be very slow to compile at times. It was lucky that incremental compilation came along just at the point where it was starting to get painful. It could still be better, but it does keep improving.
The debugger still regularly gives me nonsense or nothing at all. It’s unpredictable enough, and requires complex enough code to trigger the problems, that it’s difficult to file meaningful bugs or repros against it. But I find myself “print debugging” often.
The compiler itself is relatively stable these days, but the days of SourceKitServer Crashed were long and painful. They are largely behind us now, but I still sometimes get small squalls of them (tastefully relegated to a bar across the top of the editor window, instead of splatted all over), especially when dealing with incomplete code, often with something like:
var whatever: SomeType { didSet { if whatever != oldValue { updateTheWhatever( // haven't finished typing yet, but now everything explodes
New versions of Swift also involve source-breaking changes. The code migrators help somewhat, but in my experience don’t catch everything. I’ve heard a lot of fear and uncertainty about this, but it happened rarely, and when it did, I found that it didn’t take more than a day to fix up a build. Which is obviously a day more than I’d like, but the tradeoff is much better tools, so it’s a tradeoff I’ll make.
There is one frustrating element to that, though: external releases of an app – even TestFlight beta test releases – won’t be approved by Apple unless they’re built using an official App Store release of Xcode. And builds of the Swift compiler are tied to builds of Xcode. And nobody wants to migrate their codebase backwards through Swift versions, so once you’ve moved forwards, you’re basically screwed until Xcode ships, which, for the big release cycles for a new iOS version, is typically only a few days before iOS itself is released.
This delayed the release of Ferrite by about a month since beta testing couldn’t begin on time, solely due to Apple policy limitations. Not cool.
Swift skeptics (Sweptics?) will probably point to this as justification for avoiding it, but, even having actually lived through the experience, I’m still on board the Swift train. Let’s dig into that:
With programming, there’s a tension between “high level” and “low level”. Each have their problems. Going too low-level is like being lost in the woods, your vision cluttered by trees and visual noise. Going too high-level is like being in a plane high above the cloud layer. You look down, but all you can see is fluffy white clouds. There’s nothing solid to get a handle on.
I’ve found Swift strikes the right balance. Low-level enough to produce fast, efficient code, to be able to talk to C directly, to be able to get in and Do Real Work without wading through a sea of indirection and nonsense.
But at the same time, it’s high-level enough that you can create useful, powerful abstractions that let you get work done faster.
For some people, “abstraction” has become a dirty word in programming, perhaps due to drowning in C++ abstract base classes or Java “Enterprise-y” AbstractClassFactoryGenericBuilderFactory nonsense.
But abstraction is the key to what makes programming programming instead of declaring data, right from the first time you learn to write a loop or a function instead of repeating lines of code. All that GenericAbstractFactoryClassBuilderClass stuff is, I suspect, often the result of a broken type system, forcing the programmer to trowel down multiple thick layers of glue to connect the puzzle-pieces.
In a language rich enough to express what you actually want, you can make powerful abstractions that are very “thin”. For me personally, Python was probably the first time I really encountered this – the ease of supporting for ... in, for example – and I was a huge fan.
Swift offers many of the same opportunities – while also bringing big advantages in terms of speed and safety.3
Sweeping high-level concepts are easy to set up in Swift. For example, it’s pretty straightforward to build on GCD to create a solid implementation of Futures that can be used as easily as returning a regular value – without needing it to be baked into the language.
// This function already knows about Futures:
let example1 = getImageFromRemoteServer()
// This one doesn't, but is trivially adapted:
let example2 = Future { someLongRunningFunction(blah, blah2) }
// both return immediately without blocking, returning values that
// can be passed around, stored, mapped, etc. If you absolutely
// must block waiting for the result, you can:
imageView.image = example1.value
// But you're better off using async handlers:
example2.onSuccess { imageView2.image = $0 }
I might go into Futures in more detail some other time, but they allow you to write asynchronous code in an old-fashioned “straight line” manner which is easy to read and easy to reason about; they can bridge the gap between different styles of (a)synchronous code; can bridge error handling across threads; and even track progress and cancellation, for code that supports it (via NSProgress).
I use them extensively, not just for performing tasks in the background (e.g. rendering a Ferrite project out to an AAC file) but also, increasingly, for handling things like sequences of user input (I give an example near the end of the post).
Part of this is about the relative seamlessness of Swift code talking to C and Objective-C. As I mentioned the audio engine is not written in Swift, so this is super important. Fortunately, it’s pretty trivial to not just call C/Objective-C functions and methods, but to pass complex data back and forth, without translation layers stuck in between.
This can be something as simple as a timestamp, which is represented in Ferrite as a 64-bit fixed-point integer inside a plain C struct. Swift can access these directly, and use extensions to add math operators to them, and generally make them pleasant to use, yet because they are just vanilla structs in C, they’re safe to cross the blood-brain barrier to the CoreAudio thread in the audio engine.4
But it can also be something as complex as a sequence of audio clips the user has carefully sliced, diced, arranged, and applied fades to. These Swift classes can be passed straight into Objective-C methods (since the bridging is two-way) where it can deal with safely chopping out bits to feed to the audio thread.
But language bridging isn’t the only area where Swift mitigates impedance mismatch.
One of the promises of object-oriented coding since its inception has been simple, reusable software components. And, on one level, we do have that: we use them constantly throughout Cocoa apps, every time we use an NSSomething or UISomething.
On the other hand, I’ve found that this reuse starts to peter out at a certain level of detail. To retain its reusability, either the object starts to become too abstract and hand-wavy where you’re coding with smoke and you can’t get your hands on anything solid, or else dependencies start to creep in and it becomes tied to a specific codebase or problem domain.
With Swift, I’ve found it easier than ever to create components that follow the old UNIX philosophy of “small pieces, loosely bound”. A lot of this is to do with the http://www.wooji-juice.com/blog/swift-genius-of-protocols.html genius of protocols] (and generics, and the way they work with structs and enums as well as classes) that I’ve already written about.
This might seem like a restatement of the previous section about abstraction. But where that was about simplicity of code, this is about connectivity. To give an electronic analogy, that was about the individual components being simpler; this is more like standards where (for the most part) everything interconnects cleanly allowing you to plug a MIDI keyboard into an iPad to play music. Generics and protocols are behind both, though, so the concepts start to shade into one another.
In theory, this was always the dream of something like the C++ Standard Template Library: a catalogue of pristine algorithms ready to be applied to any datatype that conformed to some simple requirements. Of course, I never found this to be true in practice – the requirements seemed arcane, and considerable amounts of the template system’s expressiveness seemed to be largely an accident, relying on quirks and abuses of the system to achieve goals that Swift’s generics make explicit, and so the result was hideous amounts of boilerplate and obscure incantation to achieve what should be straightforward.5
This point has just become stronger and stronger as Swift has developed. Features like protocol extensions mean that when you write just a handful of lines of code to make your type a CollectionType, it automatically gains a massive suite of functionality and can be seamlessly interchanged with other CollectionTypes when used with other generic code:
struct PointlessCounter: CollectionType
{
let startIndex = 0
let endIndex = 10
subscript(index: Int) -> String { return "\(index)" }
}
Yes, it’s useless – it’s an immutable collection of ten strings – but it shows how little junk is required for Swift to consider your type a valid collection. You can not only for ... in iterate it, but perform a whole variety of interesting operations:
> PointlessCounter().reverse().joinWithSeparator(", ")
$R0: String = "9, 8, 7, 6, 5, 4, 3, 2, 1, 0"
Writing code that is a “good Swift citizen”, and fully participates in the ecosystem of Swift protocols and generic algorithms, is not just possible, but actually pretty straightforward and almost easier to do, than not.
And that’s the real benefit to it. There’s a lot of hard work and hard thinking that goes into making an app, and the less of it spent on language quirks, or rewriting code in multiple places because it’s too difficult to extract out the common elements, or trying to peer through the fog of layers of meaningless abstraction, the more you can concentrate on making the app better.
Even something as simple as being able to write a basic struct in a handful of lines – even nested & namespaced inside another class – then shove it into an Array or Dictionary, safely and consistently, frees up brainspace that would otherwise be taken up with boilerplate. Compare this:
struct Foo
{
let data = "Hello World"
}
to this (because you can’t just shove a C struct straight into an NSArray):
// In Foo.h
@interface Foo: NSObject
@property (nonatomic, readonly, strong) NSString* data;
@end
// In Foo.m
#import "Foo.h"
@implementation Foo
- (instancetype)init
{
self = [super init];
if (self != nil)
{
_data = @"Hello World";
}
return self;
}
@end
…let alone this monstrosity in modern C++.
It’s not just about writing it, it’s about coming back later and reading it and reminding yourself what’s going on. It’s about navigating around a source file and seeing the three-line definition of Foo right where it’s used, instead of having to tab through to another file (or two) and parse a pageful.
It’s about this extending out fractally to higher and higher levels so that you can end up writing code like:
@IBAction func importAudio(sender: AnyObject?)
{
let audioBrowser = storyboard?.instantiateViewControllerWithIdentifier("audioBrowser") as! AudioBrowserController
let popupSource = PopupSource(sender)
audioBrowser.presentIn(viewController: self, from: popupSource)
.onSuccess
{
self.insertAudio($0)
}
}
Or, have I mentioned before how much I like Cartography? It’s a library that lets you define Auto Layout constraints by writing simple (in)equalities:
constrain(label, button, container)
{
label, button, container in
label.leading == container.leadingMargin
label.trailing == button.leading
button.trailing <= container.trailingMargin
button.top == container.top + 30
button.width >= 44
label.baseline == button.baseline
}
Simple, clear, readable code that directly expresses its intention. And, while Cartography uses a bunch of crazy operator overloading, all of that is applied to special proxy objects. Instead of polluting UIViews with that stuff, it’s safely quarantined inside the constrain() block.
In conclusion: Eh, go back to the top and read the tl;dr :-) Congrats Player 1 on making it to the end of the article. Your Prince, Princess or Non-Gender-Specific Royalty, Nobleperson, or Other Person Not Raised Within An Exploitative Hereditary Aristocracy But Of Truly Noble Spirit, is In Another Castle.
44,100 frames per second – even if each frame is only 2 floating point numbers ↫
The gist of it is that audio processing is done in tiny, lightweight C++ classes that run on the CoreAudio thread and follow all the rules for realtime safety; but each one is paired with a “twin” in Objective-C that takes care of memory management, provides a clean interface, is exposed to Swift, etc. The twins communicate via by posting messages through lock-free thread-safe buffers ↫
Back when I was getting into Python, there was a lot of pointless Internet verbiage slung back and forth about the relative merits of static vs dynamic languages. I read this with interest, generally stayed out of commenting, but my feeling at the time was that the advantages of dynamic languages outweighed their (nonetheless considerable) disadvantages, as static languages of the time were a mess. Arguments were made that more-or-less the best of both worlds could be achieved with generics and a sufficiently advanced compiler – but, like boundless energy from nuclear fusion, that was a long way off. Many years have passed since then, however, and Swift is… sufficiently advanced. ↫
And some judicious use of #if defined(__cplusplus) allows them to gain some mod cons in the audio engine too. ↫
It’s been a while since I’ve dived into that stuff in earnest, since I’ve found Python, Objective-C and Swift to be more productive uses of my time, so the C++ situation may have changed. But certainly it used to suck through a straw. ↫