Hot Reloading for Swift applications!
Inject
Hot reloading workflow helper that enables you to save hours of time each week, regardless if you are usingUIKit, AppKit or SwiftUI.
TLDR: A single line of code change allows you to live code UIKit screen:
https://user-images.githubusercontent.com/26660989/161756368-b150bc25-b66f-4822-86ee-2e4aed713932.mp4
Read detailed article about this
The heavy lifting is done by the amazing InjectionIII. This library is just a thin wrapper to provide the best developer experience possible while requiring minimum effort.
I've been using it for years.
What is hot reloading?
Hot reloading is a technique allowing you to get rid of compiling your whole application and avoiding deploy/restart cycles as much as possible, all while allowing you to edit your running application code and see changes reflected as close as possible to real-time.This makes you significantly more productive by reducing the time you spend waiting for apps to rebuild, restart, re-navigate to the previous location where you were in the app itself, re-produce the data you need.
This can save you literal hours off development time, each day!
Does it add manual overhead to my workflows?
Once you configured your project initially, it's practically free.You donβt need to add conditional compilation or remove Inject code from your applications for production, it's already designed to behave as no-op inlined code that will get stripped by LLVM in non-debug builds.
Which means that you can enable it once per view and keep using it for years to come.
Integration
Initial project setup
To integrate Inject just add it as SPM dependency:
via Xcode
Open your project, click on File β Swift Packages β Add Package Dependencyβ¦, enter the repository url (https://github.com/krzysztofzablocki/Inject.git) and add the package product to your app target.
via SPM package.swift
dependencies: [
.package(
url: "https://github.com/krzysztofzablocki/Inject.git",
from: "1.2.4"
)
]
via Cocoapods Podfile
pod 'InjectHotReload'
Individual Developer setup (once per machine)
If anyone in your project wants to use injection, they only need to:- You must add "-Xlinker -interposable" (without the double quotes and on separate lines) to the "Other Linker Flags" of all targets in your project for the Debug configuration (qualified by the simulator SDK to avoid complications with bitcode), refer to InjectionForXcode documentation if you run into any issues
- Also in newer Xcode need to go to "Editor/Add Build Setting/Add User-Defined Setting" and add a value for EMITFRONTENDCOMMAND_LINES (set to "YES") to your project's Debug build settings
- Download newest version of Xcode Injection from it's GitHub Page
/Applications
- Make sure that the Xcode version you are using to compile our projects is under the default location:
/Applications/Xcode.app - Run the injection application
- Select open project / open recent from it's menu and pick the right workspace file you are using
- If everything is configured correctly you should see similar log in the console:
π InjectionIII connected /Users/merowing/work/SourceryPro/App.xcworkspace
π Watching files under /Users/merowing/work/SourceryPro
Workflow integration
You can either addimport Inject in individual files in your project or use
@_exported import Inject in your project target to have it automatically available in all its files.
SwiftUI
Just 2 steps to enable injection in yourSwiftUI Views
- call
.enableInjection()at the end of your body definition - add
@ObserveInjection var injectto your view struct
Remember you don't need to remove this code when you are done, it's NO-OP in production builds.
If you want to see your changes in action, you can enable an optional Animation variable on InjectConfiguration.animation that will be used when ever new source code is injected into your application.
InjectConfiguration.animation = .interactiveSpring()
Using Inject is demoed in this example app
UIKit / AppKit
For standard imperative UI frameworks we need a way to clean-up state between code injection phases.I create the concept of Hosts that work really well in that context, there are 2:
ViewControllerHostViewHost
Eg. If you have a SplitViewController that creates PaneA and PaneB , and you want to iterate on layout/logic code in PaneA, you modify the callsite in SplitViewController:
paneA = Inject.ViewHost(
PaneAView(whatever: arguments, you: want)
)
That is all the changes you need to do, your app now allows you to change anything in PaneAView except for its initialiser API and the changes will be almost immediately reflected in your App.
Make sure to call initializer inside Inject.ViewControllerHost(...) or Inject.ViewHost(...). Inject relies on @autoclosure to reload views when hot-reload happens. Example:
// WRONG let viewController = YourViewController() rootViewController.pushViewController(Inject.ViewControllerHost(viewController), animated: true)
// CORRECT let viewController = Inject.ViewControllerHost(YourViewController()) rootViewController.pushViewController(viewController, animated: true)
Remember you don't need to remove this code when you are done, it's NO-OP in production builds.
Injection Hook for UIKit
depending on the architecture used in your UIKit App, you might want to attach a hook to be executed each time a view controller is reloaded.Eg. you might want to bind the UIViewController to the presenter each-time there's a reload, to achieve this you can use onInjectionHook Example:
myView.onInjectionHook = { hostedViewController in
//any thing here will be executed each time the controller is reloaded
// for example, you might want to re-assign the controller to your presenter
presenter.ui = hostedViewController
}
(Optional) Automatic Injection Script
WARNING: This script automatically modifies your Swift source code. It's provided as a convenience but use it with caution! Review the changes it makes carefully. It might not be suitable for all projects or coding styles. Consider using Xcode code snippets for more manual control.
To automatically add import Inject, @ObserveInjection var inject, and .enableInjection() to your SwiftUI views, you can add the following script as a "Run Script" build phase in your Xcode project:
#!/bin/bash
Function to modify a single Swift file
modifyswiftfile() {
local filepath="$1"
local filename=$(basename "$filepath")
local tempfile="$filepath.tmp"
# Check if the file should be processed if [[ $(grep -c ": View {" "$filepath") -eq 0 ]]; then echo "Skipping: $filename (No ': View {' found)" return fi
# Create a temporary file for modifications cp "$filepath" "$tempfile"
# 1. Add import Inject if needed if ! grep -q "import Inject" "$tempfile"; then sed -i '' -e '/^import SwiftUI/a\ import Inject' "$tempfile" fi
# 2. Add @ObserveInjection var inject if needed if ! grep -q "@ObserveInjection var inject" "$tempfile"; then sed -i '' -e '/struct.*: View {/a\ @ObserveInjection var inject' "$tempfile" fi
# 3. Add .enableInjection() just before the closing brace of the body # Find the start of var body: some View { local bodystartline=$(grep -n "var body: some View {" "$tempfile" | cut -d ':' -f 1)
if [[ -n "$bodystartline" ]]; then # Get the line number of the closing brace of the body local bodyendline=$(awk -v start="$bodystartline" ' NR == start { count = 1 } NR > start { if ($0 ~ /{/) count++ if ($0 ~ /}/) { count-- if (count == 0) { print NR exit } } } ' "$tempfile")
if [[ -n "$bodyendline" ]]; then # Check if .enableInjection() is already present if ! grep -q ".enableInjection()" "$tempfile"; then # Insert .enableInjection() before the closing brace of the body sed -i '' -e "${bodyendline}i\\ .enableInjection()" "$tempfile" fi fi fi
# Check if modifications were made and overwrite the original file if ! cmp -s "$filepath" "$tempfile"; then mv "$tempfile" "$filepath" echo "Modified: $filename" else echo "No changes for: $filename" fi
rm -f "$tempfile" }
Main script
find "$SRCROOT" -name "*.swift" -print0 | while IFS= read -r -d $'\0' filepath; do
modifyswiftfile "$filepath"
done
echo "Inject modification script completed."
iOS 12
You need to add -weak_framework SwiftUI to Other Linker Flags for iOS 12 to work.The Composable Architecture
Since the introduction of ReducerProtocol you can use Inject with TCA without support code.