HotReloading: live Swift and Objective-C injection as a Swift Package
Hot reloading as a Swift Package
At a glance
- What is it?
- HotReloading is a Swift Package that injects changed function implementations into a running app without a rebuild, built on the InjectionIII host app, with device support and a Vapor configuration path. It suits someone iterating on a real app on a simulator, and the project itself says a newer and simpler project has largely superseded it.
- Who is it for?
- HotReloading is worth trying if you are iterating on a SwiftUI or UIKit app and want the change to appear without a rebuild, because the package delivers the injection client into your target and the host app does the work of compiling and shipping the new code, and because the device path with the port 8899 listener and the INJECTION_HOST override is documented in enough detail to debug. It is the wrong tool for a fresh project, and the project says so.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 152 days ago.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A client package for the InjectionIII host app
The project presents itself with a slightly triumphant title: hot reloading for Swift, Objective-C and C++. The mechanism is a division of labour, and understanding it removes most of the confusion.
The package in this repository is a client. The work of compiling your changed code and shipping it into a running process is done by InjectionIII, a separate macOS application. What the Swift Package contributes is the code that runs inside your app: the part that watches for source file changes in your home directory, opens the connection back to the host app, receives new versions of your functions and installs them.
The project description says it plainly. This project is the InjectionIII app for live code updates available as a Swift Package, and then you can inject function implementations without having to rebuild your app.
The scope of what can be injected follows the language list in the title. Swift, Objective-C and C++ all work, and for SwiftUI there is a protocol extension, HotSwiftUI, that forces screen updates so that a view reflects the new code. The project documentation frames that as being able to experience something like Xcode Previews, except for a fully functional app on an actual device.
Two limits are stated at the top of the documentation and they are worth repeating. You should only add the HotReloading product to your main target. And the documentation warns in bold not to release your app with this package configured. Both exist for the same reason: the package opens a network socket and waits for code from outside the app's normal build.
The primary language is Swift and the licence is MIT, with a LICENSE file at the root of the repository.
Adding it to a project takes a linker flag and a package reference
The setup is short, and the linker flag is the part that people miss.
To use it in your project you add this repository as a Swift Package and add the Other Linker Flags value -Xlinker -interposable. The project documentation notes that you no longer need to add a Run Script build phase, which is the older approach that used to be required.
The flag has a reason. Making a function interposable is what lets the dynamic linker redirect calls to it at run time, which is the whole trick behind injecting an implementation without recompiling the app. Without it the package builds but the injection has nothing to hook.
Two runtime details follow from that. The app watches for source file changes in your home directory, and you can change that scope by adding a comma separated list in the environment variable INJECTION_DIRECTORIES. And if you want the app to connect to InjectionIII.app when you are using the simulator, you add the environment variable INJECTION_DAEMON to your scheme.
There is one Xcode quirk worth knowing before you start, because it produces a confusing failure. Due to a quirk of how Xcode enables a debug build of Swift Packages, your configuration needs to contain the string Debug. That is not a suggestion about naming, it is a constraint on the build configuration name in the Xcode project, and a configuration called something else will not build the package in a way the runtime can use.
The platform restriction is stated in the same breath. On an M1 or M2 Mac, this project only works with an iOS or tvOS 14 or later simulator.
If you want something to look at before wiring it into your own code, there is a fork of SwiftUI-Kit that is already set up as an example project.
Device injection needs two Terminal commands and a listening port
Injecting into a real device rather than a simulator is the part of this project that is genuinely different from the ordinary hot reload story, and it has a setup cost.
This version of the project and its dependencies support injection on a real iOS or tvOS device. The mechanism is that the device connects to InjectionIII.app, which needs to be version 4.6.0 or above. To opt into receiving remote connections from a device, you type two commands into a Terminal and then restart the app:
$ rm ~/Library/Containers/com.johnholdsworth.InjectionIII/Data/Library/Preferences/com.johnholdsworth.InjectionIII.plist
$ defaults write com.johnholdsworth.InjectionIII deviceUnlock anyThe first command looks odd until you read the note that follows it. If you have used the App Store version of InjectionIII in the past, the binary releases have a different preferences file, and the two can get confused and prevent the new preference from taking effect. That is why the rm command can be necessary.
If the device still does not connect, the project documentation tells you to check that the app is listening on port 8899, and gives the netstat output to look for:
% netstat -an | grep LIST | grep 88
tcp4 0 0 127.0.0.1.8898 *.* LISTEN
tcp4 0 0 *.8899 *.* LISTENTwo ports appear there and they are doing different jobs. 8898 is bound to the loopback address only, and 8899 is bound to all interfaces, which is the one the device reaches. If the device cannot connect even then, you add an INJECTION_HOST environment variable to your scheme containing the WiFi IP address of the machine running InjectionIII.app, or you clone the project and code your Mac's IP address into the hostname variable in Package.swift and drag the clone onto your project so it takes the place of the configured Swift Package.
The trust prompts are expected. Because the HotReloading package needs to open a network socket to your Mac to receive commands and new versions of code, the first run asks you to trust that your app should be allowed to do this. At the Mac end, since InjectionIII needs to open a network port to accept the connection, you may be prompted if the macOS firewall is on.
The Vapor case is a debug-only bundle load
Server-side injection is handled differently, because a Vapor process is not an iOS app with a home directory to watch.
To use injection with a Vapor web server, the project documentation says you can download InjectionIII.app and add a line to be called as the server configures, which applies when running Vapor from inside Xcode:
#if DEBUG && os(macOS)
Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/macOSInjection.bundle")?.load()
#endifIt also adds a linker argument to your targets:
linkerSettings: [.unsafeFlags(["-Xlinker", "-interposable"],
.when(platforms: [.macOS], configuration: .debug))]The shape of that second snippet is the interesting part, and it is the same shape as the iOS setup expressed in the package manifest rather than in Xcode settings. The flags are applied with .when(platforms: [.macOS], configuration: .debug), so the interposition is enabled only for a macOS debug build. The #if DEBUG && os(macOS) guard around the bundle load does the same job for the code that loads the injection bundle.
Two things follow. The unsafeFlags entry is not something you can distribute through a Swift Package dependency, which is why the project documentation offers an alternative: add this Swift package as a dependency to the Package.swift of the App target instead. And the whole arrangement is conditioned on a debug configuration, which is the mechanism by which the project avoids the release hazard it warns about twice.
This is also where the dependency direction becomes visible. The injection bundle lives inside the InjectionIII application bundle on disk, and the Vapor server loads it from that path, so the host application has to be installed and the server has to find it there.
The editor integration and the pieces this package borrows
Two smaller capabilities are worth knowing about, and one of them is a separate project.
Hot reloading from inside VSCode is possible, and the project documentation points at a separate project by another author for the setup required. The point of it is that you get a form of VSCode previews: edit in the editor, see the change in the running app.
The credits section is more revealing about the engineering. The App Tracing functionality uses the imp_implementationForwardingToSelector trampoline implementation from OliverLetterer via the SwiftTrace project, under an MIT licence. SwiftTrace in turn uses the facebook fishhook project as an alternative to the dyld_dynamic_interpose dynamic loader private API, and the project source and header file are included in the framework for licensing details.
That is the mechanism underneath the whole thing, stated without euphemism. Injection works by interposing on a function pointer at the dynamic loader level, and fishhook is the library that lets you do that without calling the private interpose API directly. The private API alternative is documented and there for reference, and the project ships the source and the licence of the implementation it uses.
Two other borrowed pieces are worth a mention for anyone auditing the licences. The Remote server in the project, which captures video from a device, includes code adapted from acj/TimeLapseBuilder-Swift. And it carries a slightly modified version of the canviz library to render dot files in an HTML canvas, under an MIT licence, with the changes listed: passing the node ID through to the node label tag, reversing the rendering of nodes and the lines linking them, and storing edge paths so they can be coloured, with the specific line numbers given in the canviz source. There is also CodeMirror for the JavaScript editor used to evaluate code in the Xprobe browser, under an MIT licence.
The repository tree is small and matches the packaging: Package.swift at the root, a Contents/ directory for the resources that get loaded into the app, Sources/ for the code, and three scripts, copy_bundle.sh, fix_previews.sh and start_daemon.sh.
Where HotReloading is the wrong tool
The first limit is stated by the author in the first paragraph of the documentation. While this was once a way of using the InjectionIII.app on real devices and for its own development, you would not normally need to use this repository any more, because you can use the pre-built bundles with the copy_bundle.sh script. And it has also been largely superseded by the newer and simpler InjectionNext project.
So the project itself points you at two alternatives, and it is worth understanding the difference between them. The copy_bundle.sh route drops a prepared bundle into your project instead of taking the package as a dependency, which avoids the Swift Package dependency entirely but also removes the version pinning you get from a package manager. InjectionNext is a different design in the same author's line of work, and the recommendation is that if you are starting now you should look there rather than adopt this.
The second limit is the platform constraint. On Apple silicon, the package works only with an iOS or tvOS 14 or later simulator. There is no macOS application target here, and the only path to a macOS process is the Vapor case, which needs the host application installed at a fixed path.
The third is the nature of the mechanism. Injection replaces an implementation at run time, which means the code in the running process is not the code the compiler checked, and it means the app is waiting on a socket for instructions from a developer machine. The documentation is explicit about the consequences: the app must be trusted to open a network socket, the host machine may prompt for firewall permission, and the package must not ship in a release build. A project with a strict release checklist, a security review, or a team that does not want a debug-time network listener in the app has to treat that as a constraint rather than a detail.
The fourth is the configuration trap. The requirement that the configuration name contains the string Debug is a workaround for an Xcode quirk rather than a design choice, and it will surprise anyone whose project uses a different configuration naming. It is documented, but it is the kind of thing that costs an afternoon.
The fifth is the maturity of the surrounding ecosystem. The repository has no GitHub releases, so there is no tagged version to depend on, and the last push was on 2026-05-25. The dependency on InjectionIII.app is a separate installation with its own version requirement of 4.6.0 or above for device injection, and that app is the component doing the real work, so its release cadence is your release cadence.
Editorial conclusion
HotReloading is worth trying if you are iterating on a SwiftUI or UIKit app and want the change to appear without a rebuild, because the package delivers the injection client into your target and the host app does the work of compiling and shipping the new code, and because the device path with the port 8899 listener and the INJECTION_HOST override is documented in enough detail to debug. It is the wrong tool for a fresh project, and the project says so. Before you adopt it, check the three things the documentation puts in front of you: that your scheme configuration name contains the string Debug, because of how Xcode enables a debug build of Swift Packages; that your platform supports it, since on M1 and M2 Macs it works only with an iOS or tvOS 14 or later simulator; and that you remove the package before shipping, since the documentation insists twice that you must not release an app with this package configured, and the mechanism is a network socket connecting to your Mac.
Frequently asked questions
How do I add HotReloading to a Swift project?
Add the repository as a Swift Package and set the Other Linker Flags to -Xlinker -interposable. You no longer need a Run Script build phase. The scheme configuration name must contain the string Debug, because of how Xcode enables a debug build of Swift Packages.
How does HotReloading connect to a physical device?
The device connects to InjectionIII.app, version 4.6.0 or above. You opt in by running two commands in a Terminal, removing the preferences plist and setting the deviceUnlock preference, then restarting the app. If it still fails, check that the app is listening on port 8899 and set the INJECTION_HOST environment variable to the WiFi IP of your Mac.
Can I use HotReloading with a Vapor server?
Yes, by loading the macOSInjection.bundle from the InjectionIII application at server configure time, guarded by #if DEBUG && os(macOS), and adding linkerSettings with the -Xlinker -interposable flags applied only for a macOS debug configuration. Alternatively you can add the Swift package as a dependency to the App target's Package.swift.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/johnno1962-hotreloading)Was this useful?