Boost Your iOS Workflow: Hot Reloading with Inject for UIKit & SwiftUI
Since Apple introduced preview capabilities in SwiftUI
, developers have enjoyed a more interactive workflow, seeing UI changes in real-time without running the entire app. However, larger projects and alternative code editors like VSCode
or Cursor
have limited or no preview support, making it harder to visualize changes quickly.
The Inject library enables real-time updates in both SwiftUI
and UIKit
. With Inject, you can see code changes instantly without rebuilding, making development faster and more interactive. This guide walks you through setting up Inject in each framework, helping you optimize your workflow across environments.
What is Inject?
Inject is a tool that lets you see your UI changes instantly while building iOS apps—no more waiting for long compile times. Just tweak your code, and see the updates appear right away. Perfect for quickly adjusting designs or testing different layouts.
How It Works
Inject uses a smart technique called “interposing” to update your app’s code on the fly:
- Instant Code Swapping: By compiling your app with a special option, Inject can replace
Swift
functions in real-time by redirecting calls to updated code whenever you make a change. - Automatic Reloading: Inject monitors your files for changes. When you edit a file, it recompiles just that piece and loads it directly into your app without a full rebuild.
- Supports
Swift
andObjective-C
: Inject also works with Swift’s class methods and Objective-C methods, so it adapts to different coding styles. Fore more about how it works, see their documentation.
Setting Up Inject in Your Project
To get started with Inject, follow these crucial steps carefully. Each step is required for Inject to work properly.
Add Inject as a Swift Package
- In Xcode, go to File > Add Packages….
- Search for Inject’s GitHub repository or paste this URL: https://github.com/krzysztofzablocki/Inject
- Add it as a Swift Package Manager (SPM) dependency to your project.
Configure Linker Flags
- In Xcode, go to your project’s Build Settings.
- Add
-Xlinker
and-interposable
underOther Linker Flags
for all targets in theDebug configuration
.
If you’ve set up a project with Tuist, just open the Project.swift file and adjust the configuration like this!
let project = Project(
name: "MyApp",
targets: [
.target(
name: "MyApp",
destinations: .iOS,
product: .app,
...
dependencies: [
.external(name: "Inject")
],
settings: .settings(configurations: [
.debug(name: "Debug", settings: [
"OTHER_LDFLAGS": "-Xlinker -interposable"
])
])
),
...
]
)
Download and Install Inject app
- Download the latest version of Xcode Injection from its GitHub page.
- Move the downloaded app to
/Applications
.
Confirm Xcode Path
Ensure the Xcode version you’re using to build projects is located in /Applications/Xcode.app
.
Initialize Inject on App Startup
Add this code somewhere in your app to enable Inject when it starts. AppDelegate’s didFinishLaunchingWithOptions
is a typical spot, but any place that runs on launch will work:
#if DEBUG
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
//for tvOS:
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/tvOSInjection.bundle")?.load()
//Or for macOS:
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load()
#endif
Awesome, you’re almost there! Before running your project, let’s get Inject set up to watch for changes.
- Open up the Inject app you downloaded earlier. This app will keep an eye on your files for any updates.
- You’ll see the Inject icon appear in the status bar at the top of your screen. Click it, select
Open Project
, and choose your project folder.
Now that everything’s set up, open your proejct and hit Run. If Inject is connected properly, you’ll see these lines in the console
💉 InjectionIII connected /pathToYourProject/Project.xcodeproj
💉 Watching files under the directory /pathToYourProject
If you see these, you’re good to go! Inject will keep an eye on your code for any changes, and you’ll get instant updates as you work.
Now, let’s dive into the next part: how to apply Inject to both UIKit
and SwiftUI
projects.
Configuring Inject with UIKit and SwiftUI
UIKit
With UIKit, you’ll need to manage state cleanup between code injection phases to ensure smooth updates. Inject provides two types of “hosts” for this: ViewControllerHost and ViewHost. These hosts wrap the target view or view controller, allowing Inject to refresh them without modifying the target directly.
How to Use ViewControllerHost Wrap your target view controller in an Inject.ViewControllerHost at the parent level, as shown below. This way, you’ll be able to iterate on the view without changing its own code, just the call site where it’s used.
Here’s how it should look:
let viewController = Inject.ViewControllerHost(YourViewController())
rootViewController.pushViewController(viewController, animated: true)
Avoid Doing This: Make sure not to wrap the view controller at the wrong level, like this:
// WRONG
let viewController = YourViewController()
rootViewController.pushViewController(Inject.ViewControllerHost(viewController), animated: true)
This won’t work as intended, since it doesn’t give Inject full control over the lifecycle of YourViewController
.
SwiftUI
Setting up Inject in SwiftUI
takes just two quick steps:
- Add
@ObserveInjection var inject
as a property in your view. - Call
.enableInjection()
at the end of your view
Here’s an example:
import Inject
struct ContentView: View {
@ObserveInjection var inject
var body: some View {
Text("Hello, Inject!")
.padding()
.enableInjection()
}
}
And you’re done! Now, Inject will update your SwiftUI
view instantly with every code change like this.
Handling Mixed UIKit and SwiftUI with Inject
If you’re using both UIKit
and SwiftUI
together, Inject may need a bit of extra handling to update views properly. For example, if you’re presenting a UIKit ViewController
in a SwiftUI
sheet, changes to the ViewController
might not refresh on the simulator immediately.
To fix this, Inject provides an onInjection
hook. This hook lets you know when code changes so you can refresh the view.
Here’s how to set it up:
Example code:
var body: some View {
VStack {
Button("Present ViewController") {
showViewController = true
}
}
.sheet(isPresented: $showViewController) {
MyViewControllerRepresentable()
}
}
With this code, updates to MyViewController
after it’s shown won’t reflect immediately in the simulator. To fix this, we can add a unique reloadId
and use onInjection
to update it when code changes.
Updated code:
@State private var reloadId = UUID()
var body: some View {
VStack {
Button("Present ViewController") {
showViewController = true
}
}
.sheet(isPresented: $showViewController) {
MyViewControllerRepresentable().id(reloadId)
}
.onInjection {
reloadId = UUID() // Forces the view to refresh on code changes
}
}
With this change, any updates you make to MyViewController
will instantly appear on the simulator after injection.
What About Production Builds?
You might be wondering: Should I remove this code for production builds? The answer is no—it won’t have any effect. As the Inject documentation notes, it’s a no-op (no operation) in production.
However, in my opinion, it’s always wise to reduce any potential risk in your code. So it’s a good idea to add a wrapper to ensure Inject only runs in debug builds. Better safe than sorry!
Here’s how to set it up:
SwiftUI Add this function to your view extension.
import SwiftUI
import Inject
extension View {
func injectEnabled() -> some View {
#if DEBUG
return self.enableInjection()
#else
return self
#endif
}
}
Here’s how to use it.
struct ContentView: View {
#if DEBUG
@ObserveInjection private var inject
#endif
var body: some View {
VStack {
Text("Hello, Inject!")
}
#if DEBUG
.injectEnabled()
#endif
}
}
UIKit
Define injectedViewController
function
func injectedViewController(_ creator: @escaping @autoclosure () -> UIViewController) -> UIViewController {
#if DEBUG
return Inject.ViewControllerHost(creator())
#else
return creator()
#endif
}
Using injectedViewController
in UIKit
let viewController = injectedViewController(ViewController())
With this setup, we can safely use Inject in development, knowing it won’t affect production builds. Now, you’re ready to enjoy faster iterations with Inject without worrying about production!
Here are example repositories for hot loading in SwiftUI and UIKit.
SwiftUI
: https://github.com/hoangnhat92/ReactionCounter
UIKit
: https://github.com/hoangnhat92/MoodTracker
Conclusion
I hope you enjoyed this article! Hot reloading isn’t new in software development—in fact, many other languages adopted it long ago. As mobile development advances, hot reloading will likely save us even more time, especially given the long build times on mobile, which can depend heavily on hardware.
Thanks for reading!
Like what you're reading?
If this article hit the spot, why not subscribe to the weekly newsletter?
Every weekend, you'll get a short and sweet summary of the latest posts and tips—free and easy, no strings
attached!
You can unsubscribe anytime.