InjectionNext Documentation

repository·main·Indexed 19 days ago

https://github.com/johnno1962/injectionnext

InjectionNext is a tool for Xcode that enables hot-reloading by updating function implementations in a running app without restarting, utilizing Apple's linker features for interposing. It supports Swift and SwiftUI development and includes an MCP server (injection-next-mcp v1.0.0) that allows AI agents to control the app, manage project watching, capture screenshots, and replay touch events.

Tokens
4.7K
Snippets
8
Records
20
Agent score
64%

What's inside InjectionNext

  1. Debug Injection with INJECTION_TRACE

    main

    You can enable detailed logging for functions within a file by adding the INJECTION_TRACE environment variable to your scheme. This allows you to see when functions are being called and helps with debugging.

    If you encounter crashes due to miscategorized argument types, add the INJECTION_TRACE_REPAIR environment variable to your scheme. This will add failing methods to a list that excludes them from logging on subsequent runs.

  2. How InjectionNext works

    main

    InjectionNext enables updating function implementations in an app without relaunching by leveraging Apple's linker code injection. The workflow consists of three parts:

    1. Preparing the dynamic library: Recompiling source files into an object file and linking them into a dynamic library (using dlopen() for loading).
    2. Loading and binding: Loading the library and redirecting call sites to the new implementations. This uses the fishhook library to rebind symbols by scanning the dynamic library's symbol table for mangled function names (Swift names typically start with $s).
    3. Forcing redisplay: Triggering the UI to reflect changes. Since code changes don't automatically trigger UI updates, you must explicitly tell the UI to refresh.

    Implementation is split between the InjectionNext.app (which manages the menu bar and controls the process) and the InjectionNext Swift package (which you add to your project to handle the socket connection, library loading, and binding).

  3. Injecting on Physical Devices

    main

    Injection on devices is opt-in.

    1. In the InjectionNext app, select the menu item Enable Devices.
    2. If you want to inject tests, select Enable testing on device. This will copy a command to your clipboard; paste this into a "Run Script" Build Phase of your main target to copy required libraries into the app bundle.
    3. When prompted, select your project's "expanded codesigning identity" from the codesigning phase of your build logs.

    Note: If a device fails to connect after unlocking, try again.

  4. Install and Setup the InjectionNext MCP Server

    main

    To allow AI agents (like Cursor) to control InjectionNext, follow these steps to build the app, enable the control server, and install the MCP server component.

    Prerequisites

    • macOS with Xcode installed
    • Node.js 18.14.1+
    • InjectionNext.app built from this repository with ControlServer support.

    1. Build InjectionNext

    Clone the repository and build the app using xcodebuild. Note that this uses ad-hoc signing for local development.

    # Clone and init submodules
    git clone <repo-url>
    cd InjectionNext
    git submodule update --init --recursive
    
    # Build the app
    cd App
    xcodebuild -project InjectionNext.xcodeproj \
      -scheme InjectionNext \
      -configuration Debug build \
      CODE_SIGN_IDENTITY="-" \
      CODE_SIGNING_REQUIRED=NO \
      CODE_SIGNING_ALLOWED=NO

    The built app is located at: ~/Library/Developer/Xcode/DerivedData/InjectionNext-*/Build/Products/Debug/InjectionNext.app. You may optionally copy it to /Applications/.

    2. Enable the ControlServer

    The TCP control server is opt-in. You must enable it via defaults before launching the app:

    # Enable
    defaults write com.johnholdsworth.InjectionNext mcpServer -bool true
    
    # Disable
    defaults delete com.johnholdsworth.InjectionNext mcpServer

    3. Install the MCP Server

    Copy the mcp-server from the application's Resources directory to a writable location and install its dependencies:

    # If installed in /Applications
    cp -r /Applications/InjectionNext/Contents/Resources/mcp-server .
    
    cd mcp-server
    npm install

    4. Configure Cursor

    Add the server to your Cursor MCP configuration at ~/.cursor/mcp.json (global) or .cursor/mcp.json (per-project):

    {
      "mcpServers": {
        "injection-next": {
          "command": "node",
          "args": ["/absolute/path/to/InjectionNext/mcp-server/index.js"]
        }
      }
    }

    Replace /absolute/path/to with your actual path.

    5. Launch InjectionNext

    Start the app before using MCP tools:

    open /Applications/InjectionNext.app
    # Build command example
    cd App
    xcodebuild -project InjectionNext.xcodeproj \
      -scheme InjectionNext \
      -configuration Debug build \
      CODE_SIGN_IDENTITY="-" \
      CODE_SIGNING_REQUIRED=NO \
      CODE_SIGNING_ALLOWED=NO
  5. Configure Local Swift Packages for Injection

    main

    If your project contains local Swift packages, you must explicitly enable interposing for their targets in Package.swift to allow injection inside the package code.

    Note on Xcode 16.2+ Bug: Due to a bug where .when(configuration: .debug) is ignored for local package linker settings, use the following workaround to check for the RUNNING_VIA_INJECTION_NEXT environment variable.

    // Standard configuration
    linkerSettings: [
        .unsafeFlags(["-Xlinker", "-interposable"], .when(configuration: .debug))
    ]
    
    // Workaround for Xcode 16.2+ bug
    var linkerSettings: [LinkerSetting] {
        let isInjectionRunning = ProcessInfo.processInfo.environment["RUNNING_VIA_INJECTION_NEXT"] != nil
        return isInjectionRunning ? [.unsafeFlags(["-Xlinker", "-interposable"])] : []
    }
  6. Use InjectionNext without SPM Dependency (Bundle Loading)

    main

    If you prefer not to add InjectionNext as a Swift Package dependency, you can use the pre-built bundles provided in the app's resources.

    Option 1: Build Phase Script

    Add a "Run Script" Build Phase (ensure "user script sandboxing" is disabled) to copy the bundles:

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

    Option 2: Manual Runtime Loading

    Add the following code to your app's startup logic:

    #if DEBUG
    if let path = Bundle.main.path(forResource: "iOSInjection", ofType: "bundle") ??
        Bundle.main.path(forResource: "macOSInjection", ofType: "bundle") {
        Bundle(path: path)!.load()
    }
    #endif
  7. How to force UI redisplay after injection

    main

    After a new dynamic library is loaded and functions are rebound, the UI will not automatically update. You must implement a mechanism to trigger a refresh. Depending on your framework, use one of the following patterns:

    SwiftUI

    Use the @ObserveInjection property wrapper (available in the HotSwiftUI and Inject packages) on an observed instance variable within your View struct. When an injection occurs, this variable changes, triggering a SwiftUI view update.

    UIKit (UIViewController)

    There are two primary ways to handle updates in legacy UIViewController subclasses:

    1. The injected() method pattern: Implement an @objc func injected() method in your view controller. InjectionNext will perform a sweep of all known class instances; if an instance was part of the last injected library and possesses this method, InjectionNext will call it. You can use this method to call viewDidLoad() or a custom configureView() method.
    2. The Hosting pattern: Use the Inject package to "host" the view controller, which automatically reinstantiates the view controller upon injection.
  8. Inject SwiftUI Views

    main

    To successfully inject SwiftUI changes, you need to make minor code changes to your Views. You can do this manually (referencing the HotSwiftUI documentation) or automatically using the Prepare SwiftUI/... menu item in the InjectionNext app.

    It is recommended to integrate either the Inject or HotSwiftUI package into your project for better results.

  9. Use InjectionNext with Cursor or VSCode

    main

    To use InjectionNext with non-Xcode editors like Cursor or VSCode, use the "...or Watch Project" menu item to select your project root. This uses InjectionIII-style log parsing.

    Requirements & Tips:

    • Do not launch Xcode from within the InjectionNext app in this mode.
    • You must have built the app in Xcode at least once previously so logs are available.
    • For Xcode 16.3+, add the custom build setting EMIT_FRONTEND_COMMAND_LINES to your project.
    • To enable automatic file watching, add the environment variable INJECTION_PROJECT_ROOT=$(SRCROOT) to your scheme.
    • Note: Injection does not work if COMPILATION_CACHE_ENABLE_CACHING is set.
  10. Use InjectionNext with rules_xcodeproj

    main

    InjectionNext transparently supports projects using rules_xcodeproj. If your Xcode builds artifacts in the separate rules_xcodeproj output base (<outputBase>/rules_xcodeproj.noindex/build_output_base/), InjectionNext automatically:

    • Resolves bazel-out/ paths to the rules_xcodeproj output base.
    • Maps aquery configuration hashes (e.g., ios_sim_arm64-fastbuild-*) to the corresponding rules_xcodeproj configs (e.g., ios_sim_arm64-dbg-*).
    • Uses the rules_xcodeproj exec root as the working directory for recompilation.

    Hot reloading works identically whether you are building via bazel run or using an Xcode project generated by rules_xcodeproj.

  11. Setup InjectionNext for Xcode

    main

    To enable code injection (updating function bodies without relaunching the app) in your Xcode project, follow these steps:

    1. Install the App: Download a binary release, move InjectionNext.app to /Applications.
    2. Launch Xcode via InjectionNext: Run InjectionNext.app and use the Launch Xcode menu item from the status bar to launch your Xcode instance.
    3. Add Dependency: Add the InjectionNext repository as a Swift Package dependency to your project. This code is only included in DEBUG builds.
    4. Configure Linker Flags: In your target's Build Settings, under Other Linker Flags, add the following (for the Debug configuration only):
      • -Xlinker
      • -interposable (Note: Add these on separate lines without double quotes).

    Your changes will take effect when you save a source file for an app that has this package as a dependency and was launched via the InjectionNext app.

    Other Linker Flags:
    -Xlinker
    -interposable
  12. Configure Other Linker Flags for Injection

    main

    To enable the indirect dispatch required for binding new implementations at runtime, you must configure your project to use interposable symbols.

    In your Xcode project settings, add the following to your Other Linker Flags:

    -Xlinker -interposable

    This ensures that function calls are dispatched through a writable section of memory, allowing fishhook to redirect the function pointer to the new implementation in the injected dynamic library.