Pareto Optimal Apple Devtools

Mahyar McDonald
Ollie
Published in
10 min readFeb 15, 2024

In our previous article, we explained how a combination of new Swift features and how we were using them led to a difficult to diagnose stability crisis at our company, Ollie. It led to us deciding to move away from these new systems and to adopt UIKit. This article goes into how Apple Developer Tools could’ve made different decisions in the past that would’ve led to better outcomes.

Warning: It’s a rant.

You held SwiftUI wrong

Steve Jobs holding an iPhone 4

Yes we definitely held it wrong. Our last article definitely has a bunch of details about Swift UI and Swift Concurrency being held completely wrong. But why is it so easy to hold it wrong, even after spending literally over 100 hours trying to understand these problems?

Why make hard mode concurrency the default?

Why make a concurrency system for fairly standard, low complexity actions have something equivalent to quantum superpositioning as the default, where %99 of await calls and Tasks execute in the order you write in the code, except when they don’t, and will execute in a different order when you apply breakpoints vs. when you don’t and about a million different special cases?

The vast majority of mobile apps don’t need this level of power to implement their insurance apps, or even their photo processing apps because most of their heavyweight operations are in easy to reason about small sections such as a DB read, network call, logging call, GPU processing call, etc that a simple set of deterministic preemptive Dispatch queues works great for. Hard mode should not be default mode, and Tasks should’ve executed in order. Have out of order execution for the pros who can declare it in a scope operator for example.

SwiftUI is an exercise of conflicting abstractions

SwiftUI I feel is an exercise of having your cake and eating it too. They want to be a declarative pure function that you could potentially reason about, but probably quickly found out that most engineers are not that smart, or find a stateless binding only view systems create a lot more boilerplate than an equivalent imperative one, so they needed to graft in a state management system on top of a system that isn’t really supposed to have state ‘live’ inside it’s stateless function in its design.

Because they wanted it to make it ‘easy’ to use and for it to ‘just work’, they hid a ton of the complexity and implicit assumptions in front of some StateObject property wrappers and opaque some View types and try really hard to hide all the identity dependencies that SwiftUI needs to work performantly. Thus all the footguns of SwiftUI easily shoot you in the foot, because the developer UX design implies things work how your used to, but actually work really badly when you try to use it how you were expecting it to.

The solution to these problems ends up being opting out of SwiftUI state management beyond some very simple cases, or learning how to hold it very, very carefully. Worse of all, it’s a magic system where you can’t inspect the library code to understand what is really happening, unlike most of Android.

This is the Era of Second System Apple

This is a pattern I’m seeing with Apple foundational technology is a repeat of an overall trend within Apple starting around Swift, which I call ‘second system Apple’. It started around iOS 7 with the completely new UX revamp & the Swift language.

First iOS System Apple

I have about 15 years of experience with apple platform tech, starting with iOS 2 when the first public iPhone SDK came out. Some things were 10 minute blog article unintuitive, like interface builder links and table view cell recycling, but after you got past the surface ugliness of Objective-C, it was a pretty well thought out compete native UI toolkit built on top of an elegant compiled reimplementation of Smalltalk that I quite admired.

UIKit & Objective-C had some warts, but everything worked predictably well, you were only 2x slower than raw C, didn’t have to contend with a garbage collector giving you random frame drops. No Samsung reimplementing the world under your feet and no web world that went for the javascript trend of the week, had multiple browser targets and bad performance.

New ‘change the world’ transitions like Grand Central Dispatch, GCC → LLVM and ARC were actually pretty smooth transitions, with simple rules on how to use them and deterministic, predictable execution with small unintuitive corners. If you fucked up in ‘normal’ ways, it led to memory leaks and slowdowns, which were recoverable and detectable and didn’t deadlock your app in innovative, fragile new ways. Autolayout was probably the least smooth transition that first system apple had.

Second System Apple: Swift & iOS 7

Then Swift & iOS 7 came out, and the reliability and complexity of what apple delivered in the iOS space changed. I was at Uber in 2016 when we did the Swift rewrite. Before the rewrite Uber’s Rider app compile time for 1 million lines of Objective-C code was 5 minutes, had a fast startup time, a small binary size and a working debugger.

After the rewrite to 1 million lines of Swift code we got 50 minutes build times, a broken debugger (which has never gotten to the same performance level and reliability that it did back then) and a dynamic linker induced startup time issue where we had to hack in static linking into swift years before it was available officially in order to not have a 1 minute pre-main startup time.

It took about 7 or 8 years for Swift to start getting to an almost equivalent developer experience to Objective-C, and what mostly got it over the hump was the apple silicon chips in the end, not a great revolution in compiler performance.

I was the manager of the iOS developer experience team at Uber when the M1 came out and I pushed hard to get iOS engineers early upgraded because it made that much of a difference. If Uber still had an Objective-C codebase then, the M1 mac probably would’ve made build times go down to almost nothing.

Swift is still missing things like OCMock to this day. Your swift previews would probably be instantaneous at this point with Objective-C, since a lot of the slowness comes from Swift’s build time issues. You can actually hack in UIKit views into Swift UI previews, and you could probably do the same with Objective-C and get a faster experience with Swift UI Previews than you do with Swift UI itself.

Swift UI and Swift Concurrency feel like repeats of the same pattern with the Swift rollout. For these to become complete systems, you will probably have to wait another 7 years until they are really ready to be %90 as good as the original systems they are replacing at this rate, while with things like ARC, Autolayout and Dispatch, maybe avoiding v1 for a year or two is the only thing you had to worry about back then.

I admit that “second system apple” might’ve been happening for a long time, as I wasn’t there for the macOS & AppKit → iOS transition, or the Carbon transitions and so on. I am curious how it was like for developers with more experience for me, and what we missed (or not) from back then. I’ve heard that UI development today in many ways still hasn’t beat the productivity of Visual Basic 6 and Pascal, even though I was a child when that era of software was around, and there are many platforms I haven’t developed with, like Visual Studio and Windows in general. You often hear from devs that the original Visual Studio is really good.

Jonathan Blow is probably on to something about how unproductive we are today in software compared to 20 years ago.

A Thought Exercise: Could these new tools compete with a pareto optimal version of themselves that had the benefits of the original systems?

There are a lot of nice features in languages like Swift, but what are we missing from our tools as a result of that complexity?

PurelyObjective

Imagine a version of Objective-C that just implemented the “easy & fast” stuff from swift and called itself PurelyObjective:

  • Had Swift’s syntax and just implemented the square bracket part of Objective-C, no C ugliness would be available.
  • Autogenerated header files from source files implicitly, no header files needed
  • Had private/public/protected
  • Had easy namespacing
  • Did API renames to get rid of all the NSWhatevers
  • Implemented swifts ADT enums (which are relatively simple in compiler land)
  • Auto created an implicit enum type for all selector calls called ‘selector enums’ for each class that you could record and feed into various class types, basically giving you a key part of the elm / TCA architecture for free as a first class citizen.
  • Implemented strong nullability (Objective-C’s version was not strongly enforced by the compiler)
  • Implemented a java-like generics system that enforced types at compile time properly
  • A container data typeset that could handle nils

Missing Features Compared to Swift

  • Needed to keep per file imports vs per module imports
  • Might still need to type out more types instead of having type inference almost everywhere significantly slowing down performance and hiding complexity that can bite you in the future.
  • You couldn’t do systems programming with it
  • You wont get operator overloading
  • Whatever else was a minor feature that significantly increased compile & debugger times

And in exchange what you got in comparison to Swift:

  • Wicked fast compiler speed
  • Rock solid IDE debugging
  • Extremely inspectable memory runtime during debugging
  • All sorts of tools like dynamic Mock Generation
  • Near instant UIKit Previews, enabled by the dynamic dispatch nature of Objective-C, delivered 5 years earlier, finally catching up with web development 10+ years later.
  • Tiny binaries that would be smaller than equivalent javascript programs and could execute on the browser and run at 60fps
  • A build system that could literally scale to 10 million lines of code without much of a sweat, and scaled linearly with CPU core count if you needed to build faster
  • Make beautiful fluid 60fps UIs on processors weaker than a raspberry pi 2 today.
  • First class interop with C & C++ via the old Objective-C(++) code base
  • 1:1 equivalence and interop with Objective-C code.

And it would’ve taken Apple a lot less manpower and a few years to implement.

Would you take that deal?

I would!

Imagine DataUIKit

Now imagine ComponentKit ^H^H^H^H DataUIKit:

  • Same SwiftUI Syntax
  • Makes UIKit equivalent View trees of what you declare
  • Is literally just inert data
  • You take the root DataUIView tree object and then call a .generate()
  • It generates a UIView and a (BindingDelegatePacket)
  • You hook into the Binding delegate
  • You take that root view and put it wherever you want.
  • ActionGraph is a public library that you can use to make equivalent stuff with SwiftUI. We basically did the same thing with RxSwift at Uber before SwiftUI was ever public. RxSwift was pretty rock solid despite it’s conceptual complexity.
  • Think SnapKit, but for all of UIKit, or code based XIB files
  • You’re done!

In exchange you get:

  • Instant DataUIKit previews
  • Serializable view trees you could unpack over the wire, creating a SDUI system for free.
  • Yes this is XIB files, but it’s code, not XML files that are not designed to be human editable!
  • All the features of UIKit, nothing is missing
  • All the performance of UIKit
  • All the debugability and legibility of UIKit
  • A view debugger that works already
  • Few to no abstraction leaks, because it’s basically equivalent to SnapKit for UIKit + a few nicer to use data binding libraries
  • Data update flows you can put legible breakpoints on
  • Way less work to implement for apple
  • You get that DOM-esque & React-esque niceness, but with way less bullshit!

SwiftRust

Instead of trying to be an everything language with Swift and end up with “New Apple Flavored C++”, Apple makes PurelyObjective, have %90-%99 of development for apps and more be in PurelyObjective, and put all the slow compiling, fast executing, hard generic binary size exploding, high control, high safety system language stuff in SwiftRust, putting innovative concurrency things that the Rust language is also trying to solve inside it.

Since it’s made by Apple, they can share a lot of compiler infrastructure, use a very similar syntax, probably reuse a lot of the same macro infrastructure, syntax traversal and linting, have first class interop and so on and avoid a shit ton of pain from shoehorning a systems language into an app development use case and make some very safe high performance system, data and engine libraries. Have 5 more years of ABI instability because no root app of 2 million lines of code is depending on it, and your targeted, high performance library can afford a longer ABI instability time. Bake the language properly.

DispatchAwait with New Actor Feature in PurelyObjective

Get compiler enforced thread execution for single threaded libraries like UIKit without quantum superposition behavior! AsyncAwait uses Dispatch and is not an easy to deadlock “cooperative” system that the computing industry moved away from in the 90s, before multithreaded processors were ever widespread! Things execute in order, and they use a lot more locking in exchange for simplicity and predictability. Leave the quantum superpositioning fancy behavior to SwiftRust, when you actually need it for that extra performance.

Next Article

All of these things won’t happen anyway, but they are the alternatives not publicly considered. Hopefully in 5 to 10 years this will all resolve itself via Apple forward fixing it, or we will all be using AI to make software in some human language → assembly way and none of this will matter anymore. To the future!

The next article has some suggestions on how Apple could forward fix SwiftUI and Swift Concurrency.

This is part of a series of blog posts where we go into:

--

--