Swift Code Injection using dyld_dynamic_interpose
Since 2012 there has been available a form of “hot reloading” for iOS developers
which allows them to “inject” code into a running application. This allows rapid
iteration over changes to a function or putting final touches to design parameters. This
has two advantages: you don’t need to rebuild and relink a potentially large project
and you don’t have to restart the application losing track of its state. Originally
developed for Objective-C (which has a set of api’s for replacing the implementations
of methods a.k.a “Swizzling”), a limited solution was found for Swift 1.0 by patching a
class’ “vtable” and a way to make this available inside the strictures of the App Store.
The Swift solution was never entirely satisfactory however as you had to explain that
only non-final methods of classes and not structs or enums could be injected. In
practice, however this proved flexible enough to be useful for people. Having recently
come across dyld_dynamic_interpose in a talk by Peter Steinberger a new version of
inject all class, struct and enum methods which makes it possible to inject SwiftUI.
How the new release of InjectionIII works
To inject code into an application the first step is to recompile the file being injected.
The correct command for this is extracted the Xcode “.xcactivitylog” files and the
resulting object file is linked to a dynamic library that is loaded into the application.
The symbol table of this newly loaded .dylib is then scanned to find all symbols that
declare functions by looking for Swift symbols that end in "fC", "yF", "lF", “tF" or SwiftUI
body properties ending in “Qrvg”. This gives us the list of functions we want to replace.
As we mentioned, the dynamic loader provides the following very handy private api:
struct dyld_interpose_tuple {
const void * _Nonnull replacement;
const void * _Nonnull replacee;
};
void dyld_dynamic_interpose(
const struct mach_header * _Nonnull mh,
const struct dyld_interpose_tuple array[_Nonnull],
size_t count) __attribute__((weak_import));
This allows us to “interpose” these new versions of these functions, relinking them so
that all call sites can call the new implementation. In order to do this all we need is the
“replacee” address of the existing function which can be found using a dlsym() call on
the symbol name. By default, linkages internal to a framework can not be interposed
but thankfully there is a linker option “-interposable” which, if used when building
a .app or .framework it preserves the information & indirection needed for interposing.
While the implementation of InjectionIII little more complicated than described above
(as you need to track previous interposes to update the “replacee” for subsequent
injections), the result is a far more powerful injection of almost any Swift code. The
resulting developer experience using InjectionIII is completely different in that it almost
feels as if you no longer save the source file to the disk but directly into the application!
Injecting SwiftUI
Now that we have SwiftUI previews in Xcode, why would one still need to use injection?
The first reason is Injection lets you iterate over a SwiftUI body while using live data in a
completely functional application. It's also generally faster than Xcode previews as it
only recompiles a single file at a time.
There is a gotcha however. As part of the SwiftUI implementation the type of the body
property changes as you add or remove elements in a SwiftUI interface or if you use a
modifier that changes the type of an element. As the newly injected getter function
returns a different type from that which is expected by SwiftUI it will crash. The solution
to this is to erase the type. Add the following extension somewhere in your Swift code:
private var loadInjection = {
Bundle(path: "/Applications/InjectionIII.app/Contents/
Resources/iOSInjection.bundle")!.load()
}()
extension View {
#if DEBUG
func eraseToAnyView() -> AnyView {
_ = loadInjection
return AnyView(self)
}
#else
func eraseToAnyView() -> some View {
return self
}
#endif
}
Then, at the end of the SwiftUI body properties you wish to iterate over, add the
modifier: .eraseToAnyView() to force the concrete type to always be “AnyView".
Limitations
There are certain things you can’t inject in particular changes to the memory layout of
classes or structs. This means you can’t alter the type or add properties with storage
across an injection. Also, avoid adding to or reordering the methods of a non-final class.