DEV Community

Koichiro Eto
Koichiro Eto

Posted on

In Xcode development, I made it possible to hot-reload the file just by saving it.

When developing Xcode, the usual procedure is to save the file, build it, and then run the program. Normally, it is not possible to start a program and then modify its contents without exiting it. However, you can use a mechanism called "injection" to do so.

1. Try using "Injection III"

Mr. John Holdsworth release an app called "InjectionIII". It makes it easy to perform Injections.
https://github.com/johnno1962/InjectionIII
Let's try to use it.

1.1 Install the app

Install the InjectionIII app.
https://apps.apple.com/jp/app/injectioniii/id1380446739?mt=12
After it is installed, run it.
Confirm that the icon of Projection III application is displayed in the Status menu
Select "Help/README" and you will be connected to the following page.
https://github.com/johnno1962/InjectionIII

1.2. download the sample

The following is the homepage of Injection III.
http://johnholdsworth.com/injection.html

You can download a sample program from the following
http://johnholdsworth.com/GettingStarted.zip
Unzip it, so that it becomes "~/dev/GettingStarted/".

1.3. Open the sample from Xcode

Double-click GettingStarted.xcodeproj to launch it. → Open.

1.4. Connecting GettingStarted from InjectionIII Application

Select "Open project" from InjectionIII in the Status menu → Select "~/dev/GettingStarted/" → "Select Project Directory"

1.5. Run.

Execute with Cmd-R → "Master" is displayed in the simulator → Press "+" and the current time is displayed. Click on it→The current time and "CHANGEME" will be displayed.

Back to Xcode→Cmd-1→Cmd-1→Select DetailViewController.swift→Change "CHANGEME" to "CHANGED!" for example→Cmd-S to save, then the "CHANGEME" on the screen will immediately change to "CHANGED! This is called Injection. This is called Injection.

2. Try injecting from your own program

2.1. Creating an app

First, develop some simple app. It should be something that displays text.
Start Xcode→Create a new Xcode Project→iOS→"Single View App"→Next→Product Name: "InjectionTest", User Interface: Storyboard→Next→specify "~/dev"→. Create
In ViewController.swift, add show() as follows so that it will be called by viewDidLoad().

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        show()
    }
    func show() {
        let button = UIButton(frame: CGRect(x: 40, y: 100, width: 200, height: 100))
        button.backgroundColor = .cyan
        button.setTitle("Hello, world!", for: .normal)
        button.setTitleColor(.black, for: .normal)
        view.addSubview(button)
    }
}

Let's try it out at this stage: Cmd-R -> Build and run, then Simulator will start up and display "Hello, world!".

Go back to ViewController.swift and change "Hello, world!" to "Hello, Japan!" and save it with Cmd-S, but the change is not reflected.

When I Cmd-R the app again, the simulator closes and starts up again, and this time it's changed to "Hello, Japan!". Normally, we would exit the app and reload it in this way. Because the build is fast, this cycle can be done in about 3 seconds. In practical terms, you may not have many complaints.

2.2. Set up Linker Flags

Back to Xcode, Cmd-1→Select the project for InjectionTest→PROJECT: InjectionTest→Build Settings→Linking→Other Linker Flags→When you hover the cursor over it, a triangle appears on the left side→Click Press "+" on the right of Debug→Debug→Any Architecture | Any SDK: "-Xlinker -interposable"→Return to confirm.

2.3. Add a Bundle

Add Bundle to AppDelegate.swift.

        #if DEBUG
        Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
        #endif

For reference, this is the entirety of the applicable method of AppDelegate.swift.

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        #if DEBUG
        Bundle(path: "/Applications/InjectionIII.app/Contents/Resources/iOSInjection.bundle")?.load()
        #endif
        return true
    }

2.4. Add injected()

Add a method called injected to ViewController.swift.

    @objc func injected() {
        show()
    }

For reference, this is the entirety of the ViewController class.

class ViewController: UIViewController {
    @objc func injected() {
        show()
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        show()
    }
    func show() {
        let button = UIButton(frame: CGRect(x: 40, y: 100, width: 200, height: 100))
        button.backgroundColor = .cyan
        button.setTitle("Hello, world!", for: .normal)
        button.setTitleColor(.black, for: .normal)
        view.addSubview(button)
    }
}

2.5. Specify the Project for InjectionIII

Select "Open project" from InjectionIII in the Status menu → Select "~/dev/InjectionTest" → "Select Project Directory"

2.6 Run

Cmd-R→Simulator is started and "Hello, world!" is displayed. The output is as follows.

💉 Injection connected 👍
💉 Watching /Users/eto/dev/InjectionTest/**

2.7. Editing.

In ViewController.swift, let's edit the corresponding line as follows. Change "world" to "Japan".

        button.setTitle("Hello, Japan!", for: .normal)

Cmd-S, save the file. Then, "Hello, world!" on the simulator changes to "Hello, Japan!" immediately (in about 1 second). The output looks like the following.

💉 *** Compiling /Users/eto/dev/InjectionTest/InjectionTest/ViewController.swift ***
💉 Loading .dylib ...
objc[31231]: Class _TtC13InjectionTest14ViewController is implemented in both /Users/eto/Library/Developer/CoreSimulator/Devices/97670822-70F9-46B8-87F7-5545DF54E516/data/Containers/Bundle/Application/82DDD3CB-9924-4E5C-BCCC-1AE2A8A9E3AD/InjectionTest.app/InjectionTest (0x107b53b40) and /var/folders/94/shwk5bk14l5fx43cggr_n04m0000gn/T/com.johnholdsworth.InjectionIII/eval106.dylib (0x110d9c280). One of the two will be used. Which one is undefined.
💉 Loaded .dylib - Ignore any duplicate class warning ^
💉 Injected 'ViewController'
💉 Replacing InjectionTest.ViewController.__allocating_init(coder: __C.NSCoder) -> Swift.Optional<InjectionTest.ViewController>
💉 Replacing InjectionTest.ViewController.__allocating_init(nibName: Swift.Optional<Swift.String>, bundle: Swift.Optional<__C.NSBundle>) -> InjectionTest.ViewController
💉 Replacing InjectionTest.ViewController.viewDidLoad() -> ()
💉 Replacing InjectionTest.ViewController.show() -> ()
💉 Replacing InjectionTest.ViewController.injected() -> ()
💉 Class ViewController has an @objc injected() method. Injection will attempt a "sweep" of all live instances to determine which objects to message. If this crashes, subscribe to the global notification "INJECTION_BUNDLE_NOTIFICATION" to detect injections instead.

To explain what's happening internally, the InjectionIII app is constantly watching ViewController.swift and when it detects an edit, the app immediately compile it, load it dynamically, and replace the method. After that, injected() is called and the display is alternated. In this way, we can rewrite the method dynamically while the program is running, which is thought to improve the efficiency of program development.

The file so far is shown below.
You should be able to run the program as it is.
https://github.com/eto/InjectionTest

done!

Top comments (0)