When working on a medium to large iOS app, it can be daunting to constantly rebuild and manually go through app screens just to test your changes. Yes, Xcode previews exist, but in my experience, they can be slow on larger projects. They also require real code in the preview setup, which can be tricky to get right if you use dependency injection, since the code that registers all the dependencies probably will not run, often leading to crashes.
Enter InjectionNext
InjectionNext is an app that uses the -interposable linker feature to dynamically swap classes so that changes are reflected without rebuilding the app.
To set it up in a Bazel-based iOS project, I recommend the following:
Integrate the InjectionNext Swift package using
rules_swift_package_manager.Make sure to set the
-interposablelinker flag in debug mode only on yourios_applicationtarget:
linkopts = select({
# InjectionNext hot reload needs debug app symbols to stay
# interposable so injected dylibs can rebind calls to replacement
# implementations instead of always hitting the original image.
"//:debug": ["-interposable"],
"//conditions:default": [],
}),
- Similar to the linker flag, make sure that InjectionNext is linked to your app only in debug mode on the
ios_applicationtarget:
deps = [":App.library"] + select({
"//:debug": ["@swiftpkg_injectionnext//:InjectionNext"],
"//conditions:default": [],
}),
- Finally, you need the InjectionNext macOS app. Launch it, then click the option to launch Xcode from there.
NOTE: Please use the version I linked above or newer, because this is the version where my patch to make it work with rules_xcodeproj landed.
Further Source Changes Needed
Unfortunately, this is still not enough. To make everything work, you need to add @objc func injected() to every UIKit view or view controller where you call functions like setNeedsLayout() and layoutIfNeeded().
Of course, doing that manually for every view is tedious. The solution is either to integrate Inject, a Swift package that does this for you and also handles SwiftUI, or to write your own Swift macro, such as @HotReloadable, which you apply to the relevant types and which generates this code for you in debug mode.
Conclusion
I know this can seem like a lot of work, but I firmly believe that it quickly starts paying dividends as soon as you start iterating on your app, since it saves so much time.
NOTE: To make the integration with rules_swift_package_manager work, you need a release that includes my fix for collecting .s files.