Skip to content

Commit 56e281f

Browse files
feat: enhance ReactHostHelper with UI thread management
fix: add missing import for RCTReloadCommand in ReactHostHelper.h
1 parent e0a055a commit 56e281f

3 files changed

Lines changed: 72 additions & 23 deletions

File tree

ios/Modules/Helper/ReactHostHelper.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
//
77

88
#import <Foundation/Foundation.h>
9+
#import <React/RCTReloadCommand.h>
910

1011
NS_ASSUME_NONNULL_BEGIN
1112

1213
@interface ReactHostHelper : NSObject
1314

1415
- (nullable id) moduleForClass: (Class) clazz;
1516
- (void) reloadClientWithState;
17+
- (void) reload;
18+
- (void) updateBundleURL: (nonnull NSURL*) bundleURL;
1619
- (BOOL) isReactAppActive;
1720
- (void) emitEvent: (nonnull NSString*) eventName payload: (nullable id) payload;
1821

ios/Modules/Helper/ReactHostHelper.mm

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
#import "ReactHostHelper.h"
99
#import <ReactCommon/RCTHost.h>
10-
#import <React/RCTReloadCommand.h>
10+
#import <ReactCommon/RCTHost+Internal.h>
1111
#import "RCTDefaultReactNativeFactoryDelegate.h"
1212
#import "RCTReactNativeFactory.h"
1313
#import "MendixNative-Swift.h"
@@ -17,18 +17,34 @@
1717

1818
@implementation ReactHostHelper
1919

20-
- (nullable id) moduleForClass: (Class) clazz {
20+
#pragma mark - Thread Helpers
21+
22+
- (void) runOnUiThread:(dispatch_block_t)block {
2123
if ([NSThread isMainThread]) {
22-
return [self getModuleForClass: clazz];
24+
block();
2325
} else {
24-
__block id result;
25-
dispatch_sync(dispatch_get_main_queue(), ^{
26-
result = [self getModuleForClass: clazz];
27-
});
28-
return result;
26+
dispatch_async(dispatch_get_main_queue(), block);
2927
}
3028
}
3129

30+
- (void) runOnUiThreadSync:(dispatch_block_t)block {
31+
if ([NSThread isMainThread]) {
32+
block();
33+
} else {
34+
dispatch_sync(dispatch_get_main_queue(), block);
35+
}
36+
}
37+
38+
#pragma mark - Module Access
39+
40+
- (nullable id) moduleForClass: (Class) clazz {
41+
__block id result;
42+
[self runOnUiThreadSync:^{
43+
result = [self getModuleForClass: clazz];
44+
}];
45+
return result;
46+
}
47+
3248
- (nullable id) getModuleForClass: (Class) clazz {
3349
RCTHost *reactHost = [self currentHost];
3450
RCTModuleRegistry *moduleRegistry = [reactHost moduleRegistry];
@@ -50,16 +66,26 @@ - (void) reloadClientWithState {
5066
[mxReload emitOnReloadWithState];
5167
}
5268

69+
- (void) reload {
70+
[self runOnUiThread:^{
71+
[[self currentHost] reload];
72+
}];
73+
}
74+
75+
- (void) updateBundleURL:(NSURL *)bundleURL {
76+
[self runOnUiThread:^{
77+
[[self currentHost] setBundleURLProvider:^NSURL * _Nullable{
78+
return bundleURL;
79+
}];
80+
}];
81+
}
82+
5383
- (BOOL)isReactAppActive {
54-
if ([NSThread isMainThread]) {
55-
return [self currentHost] != nil;
56-
} else {
57-
__block bool result;
58-
dispatch_sync(dispatch_get_main_queue(), ^{
59-
result = [self currentHost] != nil;
60-
});
61-
return result;
62-
}
84+
__block BOOL result;
85+
[self runOnUiThreadSync:^{
86+
result = [self currentHost] != nil;
87+
}];
88+
return result;
6389
}
6490

6591
- (void)emitEvent:(nonnull NSString *)eventName payload:(nullable id)payload {

ios/Modules/ReactNative.swift

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ open class ReactNative: NSObject, RCTReloadListener {
5252
DevHelper.setDebugMode(enabled: AppPreferences.devModeEnabled && AppPreferences.remoteDebuggingEnabled)
5353

5454
showSplashScreen()
55-
55+
56+
// Register reload listener to intercept ALL reload triggers (dev menu, programmatic, etc.)
57+
// Only in developer apps or when three-finger gestures are enabled
5658
if mendixApp.isDeveloperApp || mendixApp.enableThreeFingerGestures {
5759
DispatchQueue.main.async {
5860
RCTRegisterReloadCommandListener(self)
@@ -79,6 +81,10 @@ open class ReactNative: NSObject, RCTReloadListener {
7981
}
8082

8183
// MARK: - Reload Methods
84+
85+
/// Reloads the React Native instance. Always allowed - used for OTA updates.
86+
/// - Note: This is the main entry point for app-triggered reloads (OTA, programmatic).
87+
/// No permission check - OTA updates must work in production apps.
8288
public func reload() {
8389
guard let mendixApp = mendixApp else { return }
8490

@@ -93,24 +99,38 @@ open class ReactNative: NSObject, RCTReloadListener {
9399
if response.status == "SUCCESS", let version = response.runtimeInfo?.version {
94100
MendixBackwardsCompatUtility.update(version)
95101
}
96-
self?.reloadWithBridge()
102+
self?.triggerReload()
97103
}
98104
} else {
99-
reloadWithBridge()
105+
triggerReload()
100106
}
101107
}
102-
103-
private func reloadWithBridge() {
104-
RCTTriggerReloadCommandListeners("Reload command from app")
108+
109+
private func triggerReload() {
110+
showSplashScreen()
111+
// Note: This bypasses RCTReloadListener notifications (only dev menu Cmd+R triggers those)
112+
ReactHostHelper().reload()
113+
}
114+
115+
private func updateBundleURL(_ url: URL) {
116+
self.bundleUrl = url
117+
ReactHostHelper().updateBundleURL(url)
105118
}
106119

120+
/// Reloads the React Native instance while preserving state.
121+
/// Guarded - only works in developer apps or when three-finger gestures are explicitly enabled.
122+
/// - Note: This is used for gesture-triggered reloads, not OTA updates.
107123
public func reloadWithState() {
108124
ReactHostHelper().reloadClientWithState()
109125
}
110126

111127
// MARK: - RCTReloadListener
128+
/// Intercepts ALL reload triggers (dev menu Cmd+R, programmatic, DevTools, etc.)
129+
/// Only called if listener was registered (i.e., isDeveloperApp || enableThreeFingerGestures)
112130
@objc public func didReceiveReloadCommand() {
113131
showSplashScreen()
132+
// Note: Actual reload is handled by RCTHost automatically
133+
// This callback is just for pre-reload side effects
114134
}
115135

116136
// MARK: - Debugging Methods

0 commit comments

Comments
 (0)