johnno1962 / InjectionNext

Fourth evolution of Code Injection for Xcode
MIT License
68 stars 2 forks source link

InjectionNext

The fourth evolution of Code Injection for Xcode

Using a feature of Apple's linker this implementation of Code Injection allows you to update the implementation (i.e. body) of functions in your app without having to relaunch it. This can save a developer a significant amount of time tweaking code or iterating over a design.

This repo is a refresh of the InjectionIII app that uses a different technique to determine how to rebuild source files that should be faster and more reliable for very large projects. Gone is the involved parsing of Xcode build logs (if you can locate them) and messy escaping of special characters in filenames. A new app is used to launch Xcode with a SourceKit debugging flag enabled which provides all the information you need to be able to recompile files and then the runtime implementation of injection included in the InjectionLite package looks after the rest.

The basic MO is to build the app in the App directory, or download one of the binary releases in this repo, quit Xcode and run the resulting InjectionNext.app and use that to re-launch Xcode using the menu item Launch Xcode from the status bar. You then add this repo as a Swift package dependency of your project and that should be all that is required for injection in the simulator, injection on devices and injection of a MacOS app. No more code changes required to load binary code bundles etc and you can leave the InjectionNext package configured into your project permanently as its code is only included for a DEBUG build. Your code changes take effect when you save a source for an app that has this package as a dependency and has connected to the InjectIonNext app which has launched Xcode.

As ever, it is important to add the options -Xlinker and -interposable (without double quotes and on separate lines) to the "Other Linker Flags" of the targets of your project (for the Debug configuration only) to enable "interposing". Otherwise, you will only be able to inject non-final class methods. To inject SwiftUI sucessfully a couple of minor code changes to each View are required. Consult the https://github.com/johnno1962/HotSwiftUI README or you can make these changes automatically using the menu item "Prepare SwiftUI/".

Icon

When your app runs it should connect to the InjectionNext.app and it's icon change to orange. After that, by parsing the messages from the "supervised" launch of Xcode it is possible to know when files are saved and exactly how to recompile them for injection. Injection on a device uses the same configuration but is opt-in through the menu item "Enable Devices" (as it needs to open a network port). You also need to select the project's "expanded codesigning identity" from the codesigning phase of your build logs in the window that pops up. Sometimes a device will not connect to the app first time after unlocking it. If at first it doesn't succeed, try again.

If you'd rather not be adding a SPM dependency to your project, the app's resources contains pre-built bundles which you can copy into your app during the build by using a "Run Script/Build Phase" (while disabling the "user script sandboxing" build setting) such as the following:

export RESOURCES="/Applications/InjectionNext.app/Contents/Resources"
if [ -f "$RESOURCES/copy_bundle.sh" ]; then
    "$RESOURCES/copy_bundle.sh"
fi

These bundles should load automatically if you've integerated the Inject or HotSwiftUI packages into your project. Otherwise, you can add the following code to run on startup of your app:

    #if DEBUG
    if let path = Bundle.main.path(forResource:
            "iOSInjection", ofType: "bundle") ??
        Bundle.main.path(forResource:
            "macOSInjection", ofType: "bundle") {
        Bundle(path: path)!.load()
    }
    #endif

The binary bundles also integrate Nimble and a slightly modified version of the Quick testing framework to inhibit spec caching under their respective Apache licences.

To inject tests on a device: use these bundles and, when enabling the "Enable Devices" menu item select "Enable testing on device" which will add the parameters shown to the link of each dynamic library. As you do this, a command will be inserted into the clipboard which you should add to your project as a "Run Script" "Build Phase" to copy the required libraries into the app bundle.

For more information consult the original InjectionIII README or for the bigger picture see this swift evolution post.

You can run InjectionNext from the command line and have it open your project in Xcode automatically using the -projectPath option.

open -a InjectionNext --args -projectPath /path/to/project

Set a user default with the same name if you want to always open this project inside the selected Xcode on launching the app.

The colors of the menu bar icon bar correspond to:

Please note: you can only inject changes to code inside a function body and you can not add/remove or rename properties with storage or add or reorder methods on a non final class or change function arguments.

The fabulous app icon is thanks to Katya of pixel-mixer.com.