1//
2//  RNNativeViewHandler.m
3//  RNGestureHandler
4//
5//  Created by Krzysztof Magiera on 12/10/2017.
6//  Copyright © 2017 Software Mansion. All rights reserved.
7//
8
9#import "RNNativeViewHandler.h"
10
11#import <UIKit/UIGestureRecognizerSubclass.h>
12
13#import <React/RCTConvert.h>
14#import <React/UIView+React.h>
15
16#ifdef RCT_NEW_ARCH_ENABLED
17#import <React/RCTScrollViewComponentView.h>
18#else
19#import <React/RCTScrollView.h>
20#endif // RCT_NEW_ARCH_ENABLED
21
22#pragma mark RNDummyGestureRecognizer
23
24@implementation RNDummyGestureRecognizer {
25  __weak RNGestureHandler *_gestureHandler;
26}
27
28- (id)initWithGestureHandler:(RNGestureHandler *)gestureHandler
29{
30  if ((self = [super initWithTarget:gestureHandler action:@selector(handleGesture:)])) {
31    _gestureHandler = gestureHandler;
32  }
33  return self;
34}
35
36- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
37{
38  [_gestureHandler.pointerTracker touchesBegan:touches withEvent:event];
39}
40
41- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
42{
43  [_gestureHandler.pointerTracker touchesMoved:touches withEvent:event];
44}
45
46- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
47{
48  [_gestureHandler.pointerTracker touchesEnded:touches withEvent:event];
49  self.state = UIGestureRecognizerStateFailed;
50  [self reset];
51}
52
53- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
54{
55  [_gestureHandler.pointerTracker touchesCancelled:touches withEvent:event];
56  self.state = UIGestureRecognizerStateCancelled;
57  [self reset];
58}
59
60- (void)reset
61{
62  [_gestureHandler.pointerTracker reset];
63  [super reset];
64}
65
66@end
67
68#pragma mark RNNativeViewgestureHandler
69
70@implementation RNNativeViewGestureHandler {
71  BOOL _shouldActivateOnStart;
72  BOOL _disallowInterruption;
73}
74
75- (instancetype)initWithTag:(NSNumber *)tag
76{
77  if ((self = [super initWithTag:tag])) {
78    _recognizer = [[RNDummyGestureRecognizer alloc] initWithGestureHandler:self];
79  }
80  return self;
81}
82
83- (void)configure:(NSDictionary *)config
84{
85  [super configure:config];
86  _shouldActivateOnStart = [RCTConvert BOOL:config[@"shouldActivateOnStart"]];
87  _disallowInterruption = [RCTConvert BOOL:config[@"disallowInterruption"]];
88}
89
90- (void)bindToView:(UIView *)view
91{
92  // For UIControl based views (UIButton, UISwitch) we provide special handling that would allow
93  // for properties like `disallowInterruption` to work.
94  if ([view isKindOfClass:[UIControl class]]) {
95    UIControl *control = (UIControl *)view;
96    [control addTarget:self action:@selector(handleTouchDown:forEvent:) forControlEvents:UIControlEventTouchDown];
97    [control addTarget:self
98                  action:@selector(handleTouchUpOutside:forEvent:)
99        forControlEvents:UIControlEventTouchUpOutside];
100    [control addTarget:self
101                  action:@selector(handleTouchUpInside:forEvent:)
102        forControlEvents:UIControlEventTouchUpInside];
103    [control addTarget:self action:@selector(handleDragExit:forEvent:) forControlEvents:UIControlEventTouchDragExit];
104    [control addTarget:self action:@selector(handleDragEnter:forEvent:) forControlEvents:UIControlEventTouchDragEnter];
105    [control addTarget:self action:@selector(handleTouchCancel:forEvent:) forControlEvents:UIControlEventTouchCancel];
106  } else {
107    [super bindToView:view];
108  }
109
110  // We can restore default scrollview behaviour to delay touches to scrollview's children
111  // because gesture handler system can handle cancellation of scroll recognizer when JS responder
112  // is set
113#ifdef RCT_NEW_ARCH_ENABLED
114  if ([view isKindOfClass:[RCTScrollViewComponentView class]]) {
115    UIScrollView *scrollView = ((RCTScrollViewComponentView *)view).scrollView;
116    scrollView.delaysContentTouches = YES;
117  }
118#else
119  if ([view isKindOfClass:[RCTScrollView class]]) {
120    // This part of the code is coupled with RN implementation of ScrollView native wrapper and
121    // we expect for RCTScrollView component to contain a subclass of UIScrollview as the only
122    // subview
123    UIScrollView *scrollView = [view.subviews objectAtIndex:0];
124    scrollView.delaysContentTouches = YES;
125  }
126#endif // RCT_NEW_ARCH_ENABLED
127}
128
129- (void)handleTouchDown:(UIView *)sender forEvent:(UIEvent *)event
130{
131  [self reset];
132
133  if (_disallowInterruption) {
134    // When `disallowInterruption` is set we cancel all gesture handlers when this UIControl
135    // gets DOWN event
136    for (UITouch *touch in [event allTouches]) {
137      for (UIGestureRecognizer *recogn in [touch gestureRecognizers]) {
138        recogn.enabled = NO;
139        recogn.enabled = YES;
140      }
141    }
142  }
143
144  [self sendEventsInState:RNGestureHandlerStateActive
145           forViewWithTag:sender.reactTag
146            withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES]];
147}
148
149- (void)handleTouchUpOutside:(UIView *)sender forEvent:(UIEvent *)event
150{
151  [self sendEventsInState:RNGestureHandlerStateEnd
152           forViewWithTag:sender.reactTag
153            withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO]];
154}
155
156- (void)handleTouchUpInside:(UIView *)sender forEvent:(UIEvent *)event
157{
158  [self sendEventsInState:RNGestureHandlerStateEnd
159           forViewWithTag:sender.reactTag
160            withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES]];
161}
162
163- (void)handleDragExit:(UIView *)sender forEvent:(UIEvent *)event
164{
165  // Pointer is moved outside of the view bounds, we cancel button when `shouldCancelWhenOutside` is set
166  if (self.shouldCancelWhenOutside) {
167    UIControl *control = (UIControl *)sender;
168    [control cancelTrackingWithEvent:event];
169    [self sendEventsInState:RNGestureHandlerStateEnd
170             forViewWithTag:sender.reactTag
171              withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO]];
172  } else {
173    [self sendEventsInState:RNGestureHandlerStateActive
174             forViewWithTag:sender.reactTag
175              withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO]];
176  }
177}
178
179- (void)handleDragEnter:(UIView *)sender forEvent:(UIEvent *)event
180{
181  [self sendEventsInState:RNGestureHandlerStateActive
182           forViewWithTag:sender.reactTag
183            withExtraData:[RNGestureHandlerEventExtraData forPointerInside:YES]];
184}
185
186- (void)handleTouchCancel:(UIView *)sender forEvent:(UIEvent *)event
187{
188  [self sendEventsInState:RNGestureHandlerStateCancelled
189           forViewWithTag:sender.reactTag
190            withExtraData:[RNGestureHandlerEventExtraData forPointerInside:NO]];
191}
192
193@end
194