diff --git a/React/Base/RCTTouchHandler.m b/React/Base/RCTTouchHandler.m index dfb7fc9b408945..df4d2131178ada 100644 --- a/React/Base/RCTTouchHandler.m +++ b/React/Base/RCTTouchHandler.m @@ -39,6 +39,8 @@ @implementation RCTTouchHandler NSMutableArray *_reactTouches; NSMutableArray *_touchViews; + __weak UIView *_cachedRootView; + uint16_t _coalescingKey; } @@ -150,7 +152,8 @@ - (void)_updateReactTouchAtIndex:(NSInteger)touchIndex { UITouch *nativeTouch = _nativeTouches[touchIndex]; CGPoint windowLocation = [nativeTouch locationInView:nativeTouch.window]; - CGPoint rootViewLocation = [nativeTouch.window convertPoint:windowLocation toView:self.view]; + RCTAssert(_cachedRootView, @"We were unable to find a root view for the touch"); + CGPoint rootViewLocation = [nativeTouch.window convertPoint:windowLocation toView:_cachedRootView]; UIView *touchView = _touchViews[touchIndex]; CGPoint touchViewLocation = [nativeTouch.window convertPoint:windowLocation toView:touchView]; @@ -231,6 +234,26 @@ - (void)_updateAndDispatchTouches:(NSSet *)touches [_eventDispatcher sendEvent:event]; } +/*** + * To ensure compatibilty when using UIManager.measure and RCTTouchHandler, we have to adopt + * UIManager.measure's behavior in finding a "root view". + * Usually RCTTouchHandler is already attached to a root view but in some cases (e.g. Modal), + * we are instead attached to some RCTView subtree. This is also the case when embedding some RN + * views inside a seperate ViewController not controlled by RN. + * This logic will either find the nearest rootView, or go all the way to the UIWindow. + * While this is not optimal, it is exactly what UIManager.measure does, and what Touchable.js + * relies on. + * We cache it here so that we don't have to repeat it for every touch in the gesture. + */ +- (void)_cacheRootView +{ + UIView *rootView = self.view; + while (rootView.superview && ![rootView isReactRootView]) { + rootView = rootView.superview; + } + _cachedRootView = rootView; +} + #pragma mark - Gesture Recognizer Delegate Callbacks static BOOL RCTAllTouchesAreCancelledOrEnded(NSSet *touches) @@ -262,6 +285,8 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; + [self _cacheRootView]; + // "start" has to record new touches *before* extracting the event. // "end"/"cancel" needs to remove the touch *after* extracting the event. [self _recordNewTouches:touches]; @@ -333,6 +358,8 @@ - (void)reset [_nativeTouches removeAllObjects]; [_reactTouches removeAllObjects]; [_touchViews removeAllObjects]; + + _cachedRootView = nil; } }