The year is 2040, and our newest MacBook M30X processors can compile large Swift projects perceivably instantaneously, sounds pretty amazing, right?
Except, compiling the codebase is just part of our iteration cycle. Other ones include: - restarting it (or deploying it to the device) - navigating to the previous location where you were in the App - re-producing the data you need.
It doesn’t sound too bad if you have to do it once. But what if you are like me and, on a typical day, do between 200 - 500 iterations on the codebase? It adds up.
There is a better way, embraced by other platforms and achievable in the Swift/iOS ecosystem. I’ve used it for over a decade.
Do you want to save up to 10h work per week, starting today?
Hot Reloading
Hot reloading is about getting rid of compiling your whole application and avoiding deploy/restart cycles as much as possible while allowing you to edit your running application code and see changes reflected immediately.
Causing your workflow to be more productive by reducing the time you spend waiting for apps to rebuild, restart, re-navigate to the previous location where you were in the App itself, and re-produce the data you need.
This process improvement can save you literally hours of development time, each day. I tracked my work for over a month, and for me, it was between 1-2h each day.
Frankly, If saving up to 10h of development time per week doesn’t convince you to give this a try, I don’t think anything will.
What other platforms are doing?
If you only used Apple platforms, you can be surprised to learn how many platforms have embraced hot-reloading decades ago. Whether you write Node or any other JS framework, there is a setup for you to use hot-reloading. Go
also offers hot-reloading (This blog leverages that feature).
Another example is Google’s Flutter architecture, designed from the ground up to work with hot-reloading. If you talk to engineers working on Flutter, being able to live code their applications is one of the things they love the most about the Flutter developer experience. I adored it when I wrote a Spelling Bee game for The New York Times.
Microsoft recently launched Visual Studio 2022 and offers hot-reloading for both .NET
and standard C++
applications. Microsoft has been killing it 👌 over the last decade regarding dev tooling and experience, so it is not a big surprise.
What about the Apple ecosystem?
Back in 2014, when it launched, many people were in awe of Swift Playgrounds since they allowed us to quickly iterate and see the results of our code, except they didn’t work very well because it crashed, hanged, etc. didn’t support the entire iPad environment.
Shortly after they were released, I launched an open-source project called Objective-C Playgrounds that worked much faster and more reliably than official playgrounds. My idea was to design an architecture/workflow that leveraged DyCI code injection tool I’ve been using for a few years already made by Paul.
Eight years have passed since Swift Playgrounds are still here, and they got better, but are they reliable? Are people using them to drive their development?
In my experience: not really. Playgrounds tend not to be very reliable or applicable in larger projects.
SwiftUI came along, and it’s a fantastic piece of technology (albeit still buggy), it introduced the idea of Swift Previews that are very similar to Playgrounds, are they any good?
Similar story, it’s great when it works, but it works unreliably in bigger projects and tends to break more times than they work. They don’t offer you the ability to debug the code if you have any errors correctly, and as such, the adoption has been limited.
Do we need to wait for Apple?
If you have followed me for a while, you already know the answer, ABSOLUTELY NO. After all, I made a career out of building things that vanilla Apple solutions don’t solve: from language extensions like Sourcery, Xcode improvements like Sourcery Pro through LifetimeTracker and many others open-source tools.
We can leverage the same approach I’ve initially used in my 2014 Playgrounds. I’ve been leveraging it for over a decade and I’ve used it with great success in tens of Swift Projects!
Many years ago, I switched from using DyCI to InjectionForXcode, which works even better by leveraging LLVM inter-op rather than any swizzling. It’s an entirely free, open-source tool that you run in your menu bar, and it was created by the prolific engineer John Holdsworth. You should check out his book Swift Secrets.
I recognized that Playgrounds approach might have been too heavy-handed, so today, I’m open-sourcing. A very focused micro-library called Inject that, when paired with InjectionForXcode, will make your Apple development much more efficient and enjoyable!
But don’t trust just my word for it. Look at the feedback from Alexandra and Nate, who were already highly proficient before I even introduced this workflow into The Browser Company setup, which makes it even more impressive.
Inject
This small library is entirely universal, and whether you work with UIKit,
AppKit,
or SwiftUI,
you will be able to leverage it.
You don’t need to add conditional compilation or remove Inject
code from your applications for production. It turns into no-op inlined code that will get stripped by the compiling process in non-debug builds. You can integrate it once per view and keep using it for years.
Refer to GitHub repo for instructions on configuring your project. Now let’s look at the workflow options you have.
Workflows
SwiftUI
There are only two lines required to make any SwiftUI enabled for live programming, and the moment you do that, you have a faster workflow than you did with Swift Previews while being able to use actual production data.
Here’s an example of my Sourcery Pro app loaded with all my actual data and logic, allowing me to quickly iterate on the whole app design instantaneous without any restarts, reloads, or anything like that.
Just look how fast this development workflow is :mind-blown: and tell me you’d rather wait for Xcode re-builds and re-deployment each time I’m touching the code.
These two lines don’t need to be removed since they are no-op in Release builds. So once added, they can be kept in your repo forever.
UIKit / AppKit
We need a way to clean up the state between code injection phases for standard imperative UI frameworks.
I create the concept of Hosts that work well in that context. There are 2:
- Inject.ViewHost
- Inject.ViewControllerHost
How do we integrate this? We wrap the class we want to iterate on at the parent level, so we don’t modify the type we want to inject but change the parent call site.
E.g., If you have a SplitViewController
that creates PaneA
and PaneB,
and you want to iterate on layout/logic code in PaneA,
you modify the call site in SplitViewController
:
paneA = Inject.ViewHost(
PaneAView(whatever: arguments, you: want)
)
That is all the changes you need to make. Injection now allows you to change anything in PaneAView
except its initializer API. The changes will be immediately reflected in your App.
A more specific example?
- I downloaded Covid19 App
- Added
-Xlinker -interposable
toOther Linker Flags
- Swapped a single line
Covid19TabController.swift:L63
line
from:
let vc = TwitterViewController(title: Tab.twitter.name, usernames: Twitter.content)
to:
let vc = Inject.ViewControllerHost(TwitterViewController(title: Tab.twitter.name, usernames: Twitter.content))
And now, I can iterate over the controller design without any restarts of the App:
How does this work underneath?
Hosts leverage auto-closure, so each time you inject code, we create a new instance of your type with the same arguments as initially, allowing you to iterate on any code, memory layout, and everything else. The only thing that you can’t change is your initializer API.
Host changes can’t be fully inlined, so those classes are removed in Release builds. The easiest way is to make a separate commit that swaps this one-liner and then remove it at the end of your workflow.
What about logic injection?
Standard architectures like MVVM / MVC get a free logic injection, recompile your classes, and when the methods re-execute, you’ll already be using new code.
If, like me, you love PointFree Composable Architecture, you’d probably want to inject reducer code. Vanilla TCA doesn’t allow it because reducer code is a free function that isn’t as straightforward to replace with injection, but [our fork at The Browser Company] supports it.
When I initially started consulting with TBC, the first thing I wanted was to integrate Inject
and XcodeInjection
into our workflow. The company management was very supportive.
I was able to get John Holdsworth to consult with us to help improve the tool a bit and built-in injection support for TCA architecture. John blew our mind with how fast he could execute the idea and deliver stable support for reducer injection. It works very reliably.
If you switch to our fork of TCA (which we keep up-to-date), you can leverage Inject
across both UI and TCA layers.
How reliable is it?
Nothing is perfect, but I’ve been using it for over a decade now. It’s far more reliable than Apple tech (Playgrounds / Previews).
If you invest time into learning it, it will save you and your team thousands of hours!