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 and Objective-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

Configure Linker Flags

  • In Xcode, go to your project’s Build Settings.
  • Add -Xlinker and -interposable under Other Linker Flags for all targets in the Debug 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.

Demo hot reloading

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.