Skip to content

Commit d725799

Browse files
antonisclaude
andcommitted
fix(ios): fix shake detection crash and swizzle safety
UIWindow inherits motionEnded:withEvent: from UIResponder and may not have its own implementation. Using method_setImplementation directly on the inherited Method would modify UIResponder, affecting all subclasses and causing a doesNotRecognizeSelector crash. Fix by calling class_addMethod first to ensure UIWindow has its own method before replacing the IMP. Also prevent duplicate NSNotification observers on component remount, and clean up debug logging. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent e844405 commit d725799

3 files changed

Lines changed: 38 additions & 17 deletions

File tree

packages/core/ios/RNSentry.mm

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,10 @@ - (void)handleShakeDetected
306306
// that JS calls directly from startShakeListener/stopShakeListener.
307307
RCT_EXPORT_METHOD(enableShakeDetection)
308308
{
309+
// Remove any existing observer first to avoid duplicate notifications
310+
[[NSNotificationCenter defaultCenter] removeObserver:self
311+
name:RNSentryShakeDetectedNotification
312+
object:nil];
309313
[[NSNotificationCenter defaultCenter] addObserver:self
310314
selector:@selector(handleShakeDetected)
311315
name:RNSentryShakeDetectedNotification

packages/core/ios/RNSentryShakeDetector.m

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,15 @@
88
NSNotificationName const RNSentryShakeDetectedNotification = @"RNSentryShakeDetected";
99

1010
static BOOL _shakeDetectionEnabled = NO;
11-
static IMP _originalMotionEndedIMP = NULL;
1211
static BOOL _swizzled = NO;
12+
static IMP _originalMotionEndedIMP = NULL;
1313
static NSTimeInterval _lastShakeTimestamp = 0;
1414
static const NSTimeInterval SHAKE_COOLDOWN_SECONDS = 1.0;
1515

16-
// Intercepts UIWindow motion events before they continue up the responder chain.
17-
//
18-
// The iOS simulator routes shake (Cmd+Ctrl+Z) through UIWindow.motionEnded:withEvent:,
19-
// not through UIApplication.sendEvent:. React Native's dev menu also hooks UIWindow
20-
// via RCTSwapInstanceMethods. Because we swizzle from enableShakeDetection (which fires after
21-
// RN finishes loading), our IMP becomes the outermost layer: our code runs first,
22-
// then the saved original IMP (RN's dev menu handler) is called.
16+
// C function that replaces UIWindow's motionEnded:withEvent: IMP.
17+
// Uses method_setImplementation to install itself and saves the original IMP
18+
// to call afterwards, preserving the responder chain and composing with other
19+
// swizzles (e.g. RCTDevMenu in debug builds).
2320
static void
2421
sentry_motionEnded(UIWindow *self, SEL _cmd, UIEventSubtype motion, UIEvent *event)
2522
{
@@ -46,13 +43,30 @@ + (void)enable
4643
@synchronized(self) {
4744
if (!_swizzled) {
4845
Class windowClass = [UIWindow class];
49-
Method originalMethod
50-
= class_getInstanceMethod(windowClass, @selector(motionEnded:withEvent:));
51-
if (originalMethod) {
52-
_originalMotionEndedIMP = method_getImplementation(originalMethod);
53-
method_setImplementation(originalMethod, (IMP)sentry_motionEnded);
54-
_swizzled = YES;
46+
SEL sel = @selector(motionEnded:withEvent:);
47+
48+
// UIWindow may not have its own motionEnded:withEvent: — it can inherit from
49+
// UIResponder. We must ensure the method exists directly on UIWindow before
50+
// replacing its IMP, otherwise the inherited method on UIResponder would be
51+
// modified, affecting all UIResponder subclasses.
52+
Method inheritedMethod = class_getInstanceMethod(windowClass, sel);
53+
if (!inheritedMethod) {
54+
return;
5555
}
56+
57+
// class_addMethod only succeeds if UIWindow does NOT already have its own
58+
// implementation of motionEnded:withEvent:. In that case, we add a direct
59+
// implementation to UIWindow that just calls super (the inherited IMP).
60+
IMP inheritedIMP = method_getImplementation(inheritedMethod);
61+
const char *types = method_getTypeEncoding(inheritedMethod);
62+
class_addMethod(windowClass, sel, inheritedIMP, types);
63+
64+
// Now UIWindow definitely has its own motionEnded:withEvent:. Get its Method
65+
// (which may be the one we just added, or a pre-existing one from e.g. RCTDevMenu)
66+
// and replace the IMP with our interceptor.
67+
Method ownMethod = class_getInstanceMethod(windowClass, sel);
68+
_originalMotionEndedIMP = method_setImplementation(ownMethod, (IMP)sentry_motionEnded);
69+
_swizzled = YES;
5670
}
5771
_shakeDetectionEnabled = YES;
5872
}

packages/core/src/js/feedback/ShakeToReportBug.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ const defaultEmitterFactory: EmitterFactory = nativeModule => new NativeEventEmi
2525
*/
2626
export function startShakeListener(onShake: () => void, createEmitter: EmitterFactory = defaultEmitterFactory): void {
2727
if (_shakeSubscription) {
28-
debug.log('Shake listener is already active.');
2928
return;
3029
}
3130

@@ -42,14 +41,18 @@ export function startShakeListener(onShake: () => void, createEmitter: EmitterFa
4241

4342
const emitter = createEmitter(nativeModule);
4443
_shakeSubscription = emitter.addListener(OnShakeEventName, () => {
45-
debug.log('Shake detected.');
4644
onShake();
4745
});
4846

4947
// Explicitly enable native shake detection. On iOS with New Architecture (TurboModules),
5048
// NativeEventEmitter.addListener does not dispatch to native addListener:, so the
5149
// native shake listener would never start without this explicit call.
52-
(nativeModule as { enableShakeDetection?: () => void }).enableShakeDetection?.();
50+
const module = nativeModule as { enableShakeDetection?: () => void };
51+
if (module.enableShakeDetection) {
52+
module.enableShakeDetection();
53+
} else {
54+
debug.warn('enableShakeDetection is not available on the native module.');
55+
}
5356
}
5457

5558
/**

0 commit comments

Comments
 (0)