# InjectionIII: hot reload for Swift and Objective-C in the Xcode simulator

> InjectionIII recompiles edited source files into a dynamic library and loads them into a running iOS, tvOS, visionOS or macOS app, so you can change a function without a full rebuild. Here is how the mechanism works, what it cannot do, and who should stay away.

**johnno1962/InjectionIII** — Re-write of Injection for Xcode in (mostly) Swift

- Repository: https://github.com/johnno1962/InjectionIII
- Stars: 4,647 · Forks: 349
- Language: Objective-C
- License: MIT
- Published: 2026-09-23 · Updated: 2026-09-23 · Language: en
- Canonical page: https://hysenlabs.com/projects/johnno1962-injectioniii

## The problem InjectionIII removes: the rebuild between two saves

The README frames the goal plainly: injection "changes Xcode from being a 'source editor' to being a program editor where source changes are not just saved to disk but into your running program directly." That is the whole pitch. A full rebuild and relaunch of an iOS app costs tens of seconds; a tweak to a layout constant or a font size costs the same again. InjectionIII targets that loop, and it targets it specifically inside the simulator, where the app process can be reached and modified from outside.

The audience is narrow but committed. It is Swift and Objective-C developers working on UI code, view controllers, SwiftUI views, or anything that is re-executed on the next frame. It is not for people shipping server-side Swift, not for people who only build for device, and not for people who expect a general-purpose live-coding environment. The project has existed long enough that the README opens with a "Stop Press" note about Xcode 16.3, which tells you how tightly it is coupled to Xcode's internals.

## How InjectionIII recompiles and loads your file

The mechanism is described in the README in one sentence: InjectionIII "works by recompiling edited source files into a dynamic library which is then loaded into your app," and it decides how to recompile by "searching the most recent Xcode build logs for the swift-frontend compiler invocation."

That is the entire trick, and it explains almost every limitation on this page. InjectionIII does not parse your project file. It scrapes the build log, replays the compiler command it finds there with your edited file substituted, produces a dylib, and loads it. The linker flag -interposable is what makes the swap work: symbols compiled as interposable can be redirected at load time, so existing call sites start pointing at the new implementation instead of the one baked into the app binary.

Two consequences follow. First, if the build log does not contain the compiler invocation, InjectionIII has nothing to replay, which is exactly what broke with Xcode 16.3. Second, because the swap happens at the symbol level, anything that is not a global interposable symbol is out of reach. The README says private properties and methods "can't be injected directly, particularly in extensions as they are not a global interposable symbol." They sometimes change indirectly because the file around them was recompiled, which the README notes "can cause confusion."

## Installing InjectionIII and injecting your first file

Get the app from the GitHub releases page or the Mac App Store. The README says setup is now "as simple as downloading one of the github releases of the app or from the Mac App Store and adding the code below somewhere in your app to be executed on startup (it is no longer necessary to actually run the app itself)."

Add the bundle load behind a DEBUG guard. The README shows one block with the iOS path and commented alternatives for tvOS and macOS:

```swift
#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
```

Then add -Xlinker and -interposable to Other Linker Flags for the Debug configuration only. The README is explicit that they go "without double quotes and on separate lines." Without -interposable the dynamic library loads but nothing gets redirected.

Run the app in the simulator. According to the README you should see a message that a file watcher has started for your home directory, and each save of a source file in the current project should report that it was injected. If the screen does not change, the new code has not been called yet. The README's fix is an @objc func injected() hook, with this example for view controllers:

```swift
#if DEBUG
extension UIViewController {
    @objc func injected() {
        viewDidLoad()
    }
}
#endif
```

For SwiftUI the README asks for two changes per view: an @ObserveInjection property wrapper to force a redraw, and .enableInjection() at the tail of body to erase the return type. It notes that with Xcode 16's SWIFT_ENABLE_OPAQUE_TYPE_ERASURE setting, which is on by default, you do not need to erase the body type explicitly, but you still need @ObserveInjection. Both packages that provide these, HotSwiftUI and Inject, are linked from the README, and the README says the modifications optimise out to a no-op in Release builds.

## What injection cannot do: memory layout and the vtable

This is the section to read before you install anything. The README states it without hedging: "You can't inject changes to how data is laid out in memory i.e. you cannot add, remove or reorder properties with storage." Adding a stored property to a class, struct or enum is a build-and-relaunch operation, not an injection.

The same applies to methods on non-final classes. The README explains that "for non-final classes this also applies to adding or removing methods as the vtable used for dispatch is itself a data structure which must not change over injection." So refactoring a method signature on a subclass is out. Changing the body of an existing method is in.

There is a third failure mode that has nothing to do with memory: InjectionIII cannot know which code needs to re-run to update the display. That is a design boundary, not a bug, and it is why the injected() hook and @ObserveInjection exist. If you inject a view controller and nothing visibly changes, the injection may have succeeded perfectly.

Finally, the README warns that the tool "doesn't cope well with source files being added/renamed/deleted during injection," and that you may need to build and relaunch, or even close and reopen the project, to clear stale Xcode build logs. That last step is the tell: the build log is the source of truth, and stale entries in it produce stale behaviour.

## InjectionNext as a sibling, and how it differs from Inject

The repository contains an InjectionNext directory alongside the main InjectionIII app, and InjectionNext appears in the related searches for this project. The README does not describe it, so treat it as a separate artifact in the same repository rather than a documented mode of InjectionIII.

The more useful comparison is Inject, by Krzysztof Zablocki, which the README itself recommends twice: for the @ObserveInjection property wrapper and for "hosting" as an alternative to writing injected() hooks by hand. The difference in approach is where the change is applied. InjectionIII recompiles a file into a dylib and interposes symbols at the linker level, which is why it needs -interposable and why the compiler invocation matters. Inject's hosting model, as described in the blog post the README links, wraps views so that a state change triggers a rebuild of the view tree. The two overlap on SwiftUI redraws and the README presents them as complementary rather than competing: InjectionIII does the injection, and either HotSwiftUI or Inject supplies the property wrapper that makes the result visible.

## Maintenance, licensing and the Xcode 16.3 tax

The repository is not archived, and the last push was on 2026-06-14. The most recent release listed is 5.2.1RC5, tagged "Integrated MCP server," dated the same day. Before that, 5.2.0RC9 covered Xcode 16.4 and injecting generic classes, and 5.1.0 covered Xcode 16.3, xrOS, watchOS and keypath injection. The pattern is a release per Xcode version, which is the real upgrade cost: this is software that tracks another vendor's compiler and build system.

Xcode 16.3 is the concrete example. The README says the swift-frontend invocation had been logged for 10 years and that Xcode 16.3 no longer logs it by default. The workaround is to use "Editor/Add Build Setting/Add User-Defined Setting" to add EMIT_FRONTEND_COMMAND_LINES set to "YES" in the project's Debug build settings. Every developer on the team needs that setting, and every new project needs it too. Budget for the possibility that a future Xcode release requires another workaround.

Licence is MIT, which permits commercial use and modification with the copyright notice retained. The README does not discuss redistribution of the app, and the Mac App Store listing is a separate distribution channel; if you plan to bundle or fork anything, read the LICENSE file in the repository root rather than relying on this summary.

## Conclusion

Adopt InjectionIII if you iterate on UIKit or SwiftUI screens in the simulator all day and can accept the two setup steps: loading the injection bundle in a DEBUG block and adding -Xlinker -interposable to Other Linker Flags. Do not adopt it if your work is mostly data model changes, property additions, or non-final class method changes, because injection cannot alter memory layout or the vtable. Verify first that your project's Debug configuration emits the swift-frontend command lines, since Xcode 16.3 stopped logging them by default and InjectionIII depends on those logs to know how to recompile a file.

## FAQ

### Can InjectionIII add a stored property to a class?

No. The README states that you cannot add, remove or reorder properties with storage, because that changes how data is laid out in memory. For non-final classes the same restriction covers adding or removing methods, since the vtable must not change during injection.

### Why does InjectionIII not work with Xcode 16.3 by default?

It recompiles a file by searching the most recent Xcode build logs for the swift-frontend compiler invocation, and Xcode 16.3 stopped logging that by default. Adding the user-defined setting EMIT_FRONTEND_COMMAND_LINES with the value YES to the project's Debug build settings restores it.

### What linker flags does InjectionIII need?

The README asks you to add -Xlinker and -interposable to Other Linker Flags for the Debug configuration only, on separate lines and without double quotes. Interposing is what lets existing call sites pick up the newly loaded implementation.

### Does InjectionIII work on a real iPhone rather than the simulator?

The README says it can work on iOS, tvOS and visionOS devices, but you need a 4.8.0 or later GitHub release of the app, a user default written with defaults write com.johnholdsworth.InjectionIII deviceUnlock any, and a restart of the app. You also run a script in a Build Phase instead of loading the injection bundles, and must turn off User Script Sandboxing.

## Sources

- [Issues](https://github.com/johnno1962/InjectionIII/issues)
- [johnno1962/InjectionIII on GitHub](https://github.com/johnno1962/InjectionIII)
- [License: MIT](https://github.com/johnno1962/InjectionIII/blob/main/LICENSE)
- [README](https://github.com/johnno1962/InjectionIII/blob/main/README.md)
- [Releases](https://github.com/johnno1962/InjectionIII/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/johnno1962-injectioniii
