InjectionIII
Re-write of Injection for Xcode in (mostly) Swift
Install / Use
/learn @johnno1962/InjectionIIIREADME
InjectionIII.app Project
Yes, HotReloading for Swift
Chinese language README: 中文集成指南

Code injection allows you to update the implementation of functions and any method of a class, struct or enum incrementally in the iOS simulator without having to perform a full rebuild or restart your application. This saves the developer a significant amount of time tweaking code or iterating over a design. Effectively it 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.
Stop Press: Injection and Xcode 16.3
InjectionIII works by recompiling edited source files into a dynamic library
which is then loaded into your app. It determines how to recompile the file
by searching the most recent Xcode build logs for the swift-frontend
compiler invocation. Unfortunately, after this having worked for 10
years Xcode 16.3 no longer logs this information by default though it
will if you use "Editor/Add Build Setting/Add User-Defined Setting"
to add a value for EMIT_FRONTEND_COMMAND_LINES (set to "YES") to your project's
Debug build settings, then InjectionIII can continue to work as before.
InjectionNext
InjectionIII now has a start-over successor in the very similar InjectionNext project. If you encounter a limitation of InjectionIII it's recommended giving InjectionNext a try to see if the issue has been resolved there.
How to use it
Setting up your projects to use injection 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).
#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
It's also important to add the options -Xlinker and -interposable (without double
quotes and on separate lines) to the "Other Linker Flags" of targets in your project
to enable "interposing" (see the explanation below). This must be for the Debug
configuration only or you can experience problems with TestFlight.

After that, when you run your app in the simulator you should see a message saying a file watcher has started for your home directory and, whenever you save a source file in the current project it should report it has been injected. This means all places that formerly called the old implementation will have been updated to call the latest version of your code.
It's not quite as simple as that as to see results on the screen
immediately the new code needs to have actually been called.
For example, if you inject a view controller it needs to force a
redisplay. To resolve this problem, classes can implement an
@objc func injected() method which will be called after the
class has been injected to perform any update to the display.
One technique you can use is to include the following code
somewhere in your program:
#if DEBUG
extension UIViewController {
@objc func injected() {
viewDidLoad()
}
}
#endif
Another solution to this problem is "hosting" using the Inject Swift Package introduced by this blog post.
What injection can't do
You can't inject changes to how data is laid out in memory i.e.
you cannot add, remove or reorder properties with storage.
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.
Injection also can't work out what pieces of code need to
be re-executed to update the display as discussed above.
Also, don't get carried away with access control. private
properties and methods can't be injected directly, particularly
in extensions as they are not a global interposable symbol.
They generally inject indirectly as they can only be accessed
inside the file being injected but this can cause confusion.
Finally, Injection doesn't cope well with source files being
added/renamed/deleted during injection. You may need to
build and relaunch your app or even close and reopen
your project to clear out old Xcode build logs.
Injection of SwiftUI
SwiftUI is, if anything, better suited to injection than UIKit
as it has specific mechanisms to update the display but you need
to make a couple changes to each View struct you want to inject.
To force redraw the simplest way is to add a property that
observes when an injection has occurred:
@ObserveInjection var forceRedraw
This property wrapper is available in either the
HotSwiftUI or
Inject
Swift Package. It essentially contains an @Published
integer your views observe that increments with each
injection. You can use one of the following to make one
of these packages available throughout your project:
@_exported import HotSwiftUI
or
@_exported import Inject
The second change you need to make for reliable SwiftUI
injection is to "erase the return type" of the body property
by wrapping it in AnyView using the .enableInjection()
method extending View in these packages. This is because,
as you add or remove SwiftUI elements it can change the concrete
return type of the body property which amounts to a memory layout
change that may crash. In summary, the tail end of each body should
always look like this:
var body: some View {
VStack or whatever {
// Your SwiftUI code...
}
.enableInjection()
}
@ObserveInjection var redraw
You can leave these modifications in your production code as,
for a Release build they optimise out to a no-op.
Injection on an iOS, tvOS or visionOS device
This can work but you will need to actually run one of the github 4.8.0+ releases of the InjectionIII.app, set a user default to opt-in and restart the app.
$ defaults write com.johnholdsworth.InjectionIII deviceUnlock any
Then, instead of loading the injection bundles run this script in a "Build Phase": (You will also need to turn off the project build setting "User Script Sandboxing")
RESOURCES=/Applications/InjectionIII.app/Contents/Resources
if [ -f "$RESOURCES/copy_bundle.sh" ]; then
"$RESOURCES/copy_bundle.sh"
fi
and, in your application execute the following code on startup:
#if DEBUG
if let path = Bundle.main.path(forResource:
"iOSInjection", ofType: "bundle") ??
Bundle.main.path(forResource:
"macOSInjection", ofType: "bundle") {
Bundle(path: path)!.load()
}
#endif
Once you have switched to this configuration it will also work when using the simulator. Consult the README of the HotReloading project for details on how to debug having your program connect to the InjectionIII.app over Wi-Fi. You will also need to select the project directory for the file watcher manually from the pop-down menu.
Injection on macOS
It works but you need to temporarily turn off the "app sandbox" and
"library validation" under the "hardened runtime" during development
so it can dynamically load code. In order to avoid codesigning problems,
use the new copy_bundle.sh script as detailed in the instructions for
injection on real devices above.
How it works
Injection has worked various ways over the years, starting out using the "Swizzling" apis for Objective-C but is now largely built around a feature of Apple's linker called "interposing" which provides a solution for any Swift method or computed property of any type.
When your code calls a function in Swift, it is generally "statically dispatched", i.e. linked using the "mangled symbol" of the function being called. Whenever you link your application with the "-interposable" option however, an additional level of indirection is added where it finds the address of all functions being called through a section of writable memory. Using the operating system's ability to load executable code and the fishhook library to "rebind" the call it is therefore possible to "interpose" new implementations of any function and effectively stitch them into the rest of your program at runtime. From that point it will perform as if the new code had been built into the program.
Injection uses the FSEventSteam api to watch for when a source
file has been changed and scans the last Xcode build log for how to
recompile it and links a dynamic library that can be loaded into your
program. Runtime support for injection then loads the dynamic library
and scans it for the function definitions it contains which it then
"interposes" into the rest of the program. This isn't the full story as
the dispatch of non-final class methods uses a "vtable" (think C++
virtual methods) which also has to be updated but the project looks
after that along with any legacy Objective-C "swizzling".
If you are interested knowing more about how injection works the best source is either my book Swift Secrets or the new, start-over reference implementation in the [InjectionLite](https://github.com/jo
