Edit code → Cmd+S → Instantly reflected on iOS Simulator. No rebuild needed.
Zero external dependencies. Uses only built-in macOS/iOS APIs.
| Supported | Method | |
|---|---|---|
| UIKit (ViewController, UIView) | ✅ | ObjC runtime method replacement |
| SwiftUI (View body) | ✅ | Auto-generated @_dynamicReplacement |
.package(url: "https://github.com/jjh717/HotReload", branch: "main")Target dependency:
.product(name: "HotReloadClient", package: "HotReload")./install.sh /path/to/your/projectThe script automatically:
- Detects project type (Tuist / Xcode / SPM)
- Compiles & installs swiftc wrapper (
/private/tmp/HotReload/swiftc) - Adds Debug-only xcconfig settings
- Installs Build Phase script
#if DEBUG && targetEnvironment(simulator)
import HotReloadClient
#endif
func application(_ application: UIApplication, didFinishLaunchingWithOptions ...) -> Bool {
// ...
#if DEBUG && targetEnvironment(simulator)
HotReloadClient.start()
#endif
return true
}Debug build → Edit any .swift file → Cmd+S → Instantly reflected on Simulator!
Add a Hot Reload observer to your base view controller for automatic support across all VCs:
#if DEBUG && targetEnvironment(simulator)
NotificationCenter.default.addObserver(
self,
selector: #selector(injected),
name: Notification.Name("HotReloadInjected"),
object: nil
)
@objc open func injected() {
viewDidLoad()
}
#endifOverride injected() in individual VCs for custom UI refresh:
#if DEBUG && targetEnvironment(simulator)
override func injected() {
view.subviews.forEach { $0.removeFromSuperview() }
makeUI()
}
#endifNote: Methods changed by hot reload must be visible to the ObjC runtime.
privatemethods without@objcuse Swift static dispatch and won't be replaced. Add@objcto private methods you want to hot reload, or make them non-private.
Add one line to your SwiftUI View to enable hot reload:
#if DEBUG && targetEnvironment(simulator)
import HotReloadClient
#endif
struct MyView: View {
#if DEBUG && targetEnvironment(simulator)
@ObservedObject var _hotReload = HotReloadObserver.shared
#endif
var body: some View {
// Edit and press Cmd+S
}
}@_dynamicReplacement replaces the body at runtime, and HotReloadObserver triggers SwiftUI to re-evaluate it.
- FSEvents detects
.swiftfile changes - Recompiles module using cached swiftc flags →
.dylib dlopen()loads the dylib into the running app- Extracts classes from Mach-O
__objc_classlistsection method_setImplementation()replaces existing class methodsHotReloadInjectednotification →injected()called
- Detects
struct XXX: Viewand extractsvar body: some View { ... }code - Auto-generates
@_dynamicReplacement(for: body)wrapper source - Compiles with
*.debug.dyliblinkage (ensures TX symbol is an external reference) dlopen()→ Swift runtime reads__swift5_replacesection and auto-replaces body
- swiftc wrapper: Replaces Xcode's swiftc via
SWIFT_EXEC, captures all module compiler flags automatically -enable-implicit-dynamic: Makes all functionsdynamic, enabling@_dynamicReplacement*.debug.dyliblinkage: TX symbols become undefined references (U), matching the original app binary
- Simulator only:
dlopen()is not allowed on real devices (iOS security policy) - Method body changes only: Adding/removing properties or changing function signatures requires a rebuild
- State management classes: Reducer/Reactor/Coordinator classes are auto-skipped to prevent crashes
@_dynamicReplacement: Private Swift API, may change with Swift version updates- SwiftUI detection: Handles
struct Name: View,struct Name<T>: View, Equatable,extension Name: View, and multi-line declarations. Does not detect View conformance added viatypealiasor conditional conformance
Create /tmp/HotReload/hotreload.json (or /tmp/HotReload-<your-uid>/hotreload.json) to customize:
{
"excludedModules": ["MyInfraModule", "MyNetworkModule"],
"watchPaths": ["/path/to/project/Sources"]
}If you prefer manual setup instead of the install script:
// Debug only
SWIFT_USE_INTEGRATED_DRIVER[config=Debug]=NO
SWIFT_EXEC[config=Debug]=/private/tmp/HotReload/swiftc
OTHER_SWIFT_FLAGS[config=Debug]=$(inherited) -Xfrontend -enable-implicit-dynamic -Xfrontend -enable-private-imports
OTHER_LDFLAGS[config=Debug]=$(inherited) -Xlinker -interposable
DEAD_CODE_STRIPPING[config=Debug]=NO
STRIP_SWIFT_SYMBOLS[config=Debug]=NO
MIT