summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoel Fischer <joeljfischer@gmail.com>2019-04-03 13:08:55 -0400
committerGitHub <noreply@github.com>2019-04-03 13:08:55 -0400
commite222a5b87b2e003f2855b2ec2bbeaa52046579df (patch)
tree9791e9748985283f485ad12dc3284d621afa3067
parentc7fbc61729b97196c8920b5447d8845f064cabb9 (diff)
parentb067fc9a5b1af505f138918da1fdfcb14cbb9926 (diff)
downloadsdl_ios-e222a5b87b2e003f2855b2ec2bbeaa52046579df.tar.gz
Merge pull request #1209 from smartdevicelink/bugfix/issue_1207_haptic_input_checked_on_wrong_thread
Hit test for a single tap now checked on main thread
-rw-r--r--SmartDeviceLink/SDLFocusableItemLocator.m8
-rw-r--r--SmartDeviceLink/SDLTouchManager.m52
-rw-r--r--SmartDeviceLinkTests/ProxySpecs/SDLHapticManagerSpec.m148
3 files changed, 116 insertions, 92 deletions
diff --git a/SmartDeviceLink/SDLFocusableItemLocator.m b/SmartDeviceLink/SDLFocusableItemLocator.m
index 1969512e5..e3fb4d451 100644
--- a/SmartDeviceLink/SDLFocusableItemLocator.m
+++ b/SmartDeviceLink/SDLFocusableItemLocator.m
@@ -105,7 +105,7 @@ NS_ASSUME_NONNULL_BEGIN
}
NSMutableArray<SDLHapticRect *> *hapticRects = [[NSMutableArray alloc] init];
-
+
for (UIView *view in self.focusableViews) {
CGPoint originOnScreen = [self.viewController.view convertPoint:view.frame.origin toView:nil];
CGRect convertedRect = {originOnScreen, view.bounds.size};
@@ -115,7 +115,7 @@ NS_ASSUME_NONNULL_BEGIN
SDLHapticRect *hapticRect = [[SDLHapticRect alloc] initWithId:(UInt32)rectId rect:rect];
[hapticRects addObject:hapticRect];
}
-
+
SDLSendHapticData* hapticRPC = [[SDLSendHapticData alloc] initWithHapticRectData:hapticRects];
[self.connectionManager sendConnectionManagerRequest:hapticRPC withResponseHandler:nil];
}
@@ -123,7 +123,7 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark SDLFocusableItemHitTester functions
- (nullable UIView *)viewForPoint:(CGPoint)point {
UIView *selectedView = nil;
-
+
for (UIView *view in self.focusableViews) {
//Convert the absolute location to local location and check if that falls within view boundary
CGPoint localPoint = [view convertPoint:point fromView:self.viewController.view];
@@ -137,7 +137,7 @@ NS_ASSUME_NONNULL_BEGIN
}
}
}
-
+
return selectedView;
}
diff --git a/SmartDeviceLink/SDLTouchManager.m b/SmartDeviceLink/SDLTouchManager.m
index 518877e96..9a66d81e4 100644
--- a/SmartDeviceLink/SDLTouchManager.m
+++ b/SmartDeviceLink/SDLTouchManager.m
@@ -227,8 +227,8 @@ static NSUInteger const MaximumNumberOfTouches = 2;
self.currentPinchGesture = [[SDLPinchGesture alloc] initWithFirstTouch:self.previousTouch secondTouch:touch];
self.previousPinchDistance = self.currentPinchGesture.distance;
if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:pinchDidStartInView:atCenterPoint:)]) {
- UIView *hitView = (self.hitTester != nil) ? [self.hitTester viewForPoint:self.currentPinchGesture.center] : nil;
- [self.touchEventDelegate touchManager:self pinchDidStartInView:hitView atCenterPoint:self.currentPinchGesture.center];
+ UIView *hitView = (self.hitTester != nil) ? [self.hitTester viewForPoint:self.currentPinchGesture.center] : nil;
+ [self.touchEventDelegate touchManager:self pinchDidStartInView:hitView atCenterPoint:self.currentPinchGesture.center];
}
} break;
}
@@ -247,7 +247,7 @@ static NSUInteger const MaximumNumberOfTouches = 2;
return; // no-op
}
#pragma clang diagnostic pop
-
+
CGFloat xDelta = fabs(touch.location.x - self.firstTouch.location.x);
CGFloat yDelta = fabs(touch.location.y - self.firstTouch.location.y);
if (xDelta <= self.panDistanceThreshold && yDelta <= self.panDistanceThreshold) {
@@ -275,8 +275,8 @@ static NSUInteger const MaximumNumberOfTouches = 2;
_performingTouchType = SDLPerformingTouchTypePanningTouch;
if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:panningDidStartInView:atPoint:)]) {
- UIView *hitView = (self.hitTester != nil) ? [self.hitTester viewForPoint:touch.location] : nil;
- [self.touchEventDelegate touchManager:self panningDidStartInView:hitView atPoint:touch.location];
+ UIView *hitView = (self.hitTester != nil) ? [self.hitTester viewForPoint:touch.location] : nil;
+ [self.touchEventDelegate touchManager:self panningDidStartInView:hitView atPoint:touch.location];
}
} break;
case SDLPerformingTouchTypePanningTouch: {
@@ -302,9 +302,9 @@ static NSUInteger const MaximumNumberOfTouches = 2;
[self sdl_setMultiTouchFingerTouchForTouch:touch];
if (self.currentPinchGesture.isValid) {
if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:pinchDidEndInView:atCenterPoint:)]) {
- UIView *hitView = (self.hitTester != nil) ? [self.hitTester viewForPoint:self.currentPinchGesture.center] : nil;
- [self.touchEventDelegate touchManager:self pinchDidEndInView:hitView atCenterPoint:self.currentPinchGesture.center];
- self.currentPinchGesture = nil;
+ UIView *hitView = (self.hitTester != nil) ? [self.hitTester viewForPoint:self.currentPinchGesture.center] : nil;
+ [self.touchEventDelegate touchManager:self pinchDidEndInView:hitView atCenterPoint:self.currentPinchGesture.center];
+ self.currentPinchGesture = nil;
} else {
self.currentPinchGesture = nil;
}
@@ -312,8 +312,8 @@ static NSUInteger const MaximumNumberOfTouches = 2;
} break;
case SDLPerformingTouchTypePanningTouch: {
if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:panningDidEndInView:atPoint:)]) {
- UIView *hitView = (self.hitTester != nil) ? [self.hitTester viewForPoint:touch.location] : nil;
- [self.touchEventDelegate touchManager:self panningDidEndInView:hitView atPoint:touch.location];
+ UIView *hitView = (self.hitTester != nil) ? [self.hitTester viewForPoint:touch.location] : nil;
+ [self.touchEventDelegate touchManager:self panningDidEndInView:hitView atPoint:touch.location];
}
} break;
case SDLPerformingTouchTypeSingleTouch: {
@@ -333,8 +333,8 @@ static NSUInteger const MaximumNumberOfTouches = 2;
CGPoint centerPoint = CGPointCenterOfPoints(touch.location,
self.singleTapTouch.location);
if ([self.touchEventDelegate respondsToSelector:@selector(touchManager:didReceiveDoubleTapForView:atPoint:)]) {
- UIView *hitView = (self.hitTester != nil) ? [self.hitTester viewForPoint:centerPoint] : nil;
- [self.touchEventDelegate touchManager:self didReceiveDoubleTapForView:hitView atPoint:centerPoint];
+ UIView *hitView = (self.hitTester != nil) ? [self.hitTester viewForPoint:centerPoint] : nil;
+ [self.touchEventDelegate touchManager:self didReceiveDoubleTapForView:hitView atPoint:centerPoint];
}
}
@@ -420,13 +420,37 @@ static NSUInteger const MaximumNumberOfTouches = 2;
strongSelf.singleTapTouch = nil;
[strongSelf sdl_cancelSingleTapTimer];
if ([strongSelf.touchEventDelegate respondsToSelector:@selector(touchManager:didReceiveSingleTapForView:atPoint:)]) {
- UIView *hitView = (self.hitTester != nil) ? [self.hitTester viewForPoint:point] : nil;
- [strongSelf.touchEventDelegate touchManager:strongSelf didReceiveSingleTapForView:hitView atPoint:point];
+ [self sdl_getSingleTapHitView:point hitViewHandler:^(UIView * _Nullable selectedView) {
+ [strongSelf.touchEventDelegate touchManager:strongSelf didReceiveSingleTapForView:selectedView atPoint:point];
+ }];
}
});
}
/**
+ * HAX to preserve the thread on which the delegate is notified for single taps; returning on the main thread would be a breaking change. All other touch gestures currently notify the delegate on the main thread. The single tap timer runs on a background thread so when a single tap is detected the hit test needs to be done on the main thread and then the result is returned on a background thread.
+ *
+ * Checks if a single tap is inside a view. As the single tap timer is run on a background thread, the check is done on a main thread and then the result is returned on a background thread.
+ *
+ * @param point Screen coordinates of the tap gesture
+ * @param hitViewHandler A handler that returns the view the point is inside of; nil if the point does not lie inside of a view
+ */
+- (void)sdl_getSingleTapHitView:(CGPoint)point hitViewHandler:(nullable void (^)(UIView * __nullable hitView))hitViewHandler {
+ if (!self.hitTester) {
+ if (!hitViewHandler) { return; }
+ return hitViewHandler(nil);
+ }
+
+ dispatch_async(dispatch_get_main_queue(), ^{
+ UIView *hitView = [self.hitTester viewForPoint:point];
+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+ if (!hitViewHandler) { return; }
+ return hitViewHandler(hitView);
+ });
+ });
+}
+
+/**
* Cancels a tap gesture timer
*/
- (void)sdl_cancelSingleTapTimer {
diff --git a/SmartDeviceLinkTests/ProxySpecs/SDLHapticManagerSpec.m b/SmartDeviceLinkTests/ProxySpecs/SDLHapticManagerSpec.m
index c7c339ac3..497870b9d 100644
--- a/SmartDeviceLinkTests/ProxySpecs/SDLHapticManagerSpec.m
+++ b/SmartDeviceLinkTests/ProxySpecs/SDLHapticManagerSpec.m
@@ -32,23 +32,23 @@ QuickSpecBegin(SDLHapticManagerSpec)
describe(@"the haptic manager", ^{
__block UIWindow *uiWindow;
__block UIViewController *uiViewController;
-
+
__block SDLFocusableItemLocator *hapticManager;
__block SDLSendHapticData* sentHapticRequest;
-
+
__block id sdlLifecycleManager = OCMClassMock([SDLLifecycleManager class]);
__block CGRect viewRect1;
__block CGRect viewRect2;
-
+
beforeEach(^{
hapticManager = nil;
sentHapticRequest = nil;
-
+
uiWindow = [[UIWindow alloc] init];
uiViewController = [[UIViewController alloc] init];
uiWindow.rootViewController = uiViewController;
-
+
OCMExpect([[sdlLifecycleManager stub] sendConnectionManagerRequest:[OCMArg checkWithBlock:^BOOL(id value){
BOOL isFirstArg = [value isKindOfClass:[SDLSendHapticData class]];
if(isFirstArg) {
@@ -75,216 +75,216 @@ describe(@"the haptic manager", ^{
expect(sentHapticRequest).to(beNil());
});
});
-
+
context(@"when initialized with no focusable view", ^{
beforeEach(^{
hapticManager = [[SDLFocusableItemLocator alloc] initWithViewController:uiViewController connectionManager:sdlLifecycleManager];
[hapticManager updateInterfaceLayout];
});
-
+
it(@"should have no focusable view", ^{
OCMVerify(sdlLifecycleManager);
expect(sentHapticRequest.hapticRectData.count).to(equal(0));
});
});
-
+
context(@"when initialized with single view", ^{
beforeEach(^{
viewRect1 = CGRectMake(101, 101, 50, 50);
UITextField *textField1 = [[UITextField alloc] initWithFrame:viewRect1];
[uiViewController.view addSubview:textField1];
-
+
hapticManager = [[SDLFocusableItemLocator alloc] initWithViewController:uiViewController connectionManager:sdlLifecycleManager];
hapticManager.enableHapticDataRequests = YES;
[hapticManager updateInterfaceLayout];
});
-
+
it(@"should have one view", ^{
OCMVerify(sdlLifecycleManager);
-
+
int expectedCount = 1;
expect(sentHapticRequest.hapticRectData.count).to(equal(expectedCount));
-
+
if(sentHapticRequest.hapticRectData.count == expectedCount) {
NSArray<SDLHapticRect *> *hapticRectData = sentHapticRequest.hapticRectData;
SDLHapticRect *sdlhapticRect = hapticRectData[0];
SDLRectangle *sdlRect = sdlhapticRect.rect;
-
+
compareRectangle(sdlRect, viewRect1);
}
});
});
-
+
context(@"when initialized with single button view", ^{
beforeEach(^{
viewRect1 = CGRectMake(101, 101, 50, 50);
UIButton *button = [[UIButton alloc] initWithFrame:viewRect1];
[uiViewController.view addSubview:button];
-
+
hapticManager = [[SDLFocusableItemLocator alloc] initWithViewController:uiViewController connectionManager:sdlLifecycleManager];
hapticManager.enableHapticDataRequests = YES;
[hapticManager updateInterfaceLayout];
});
-
+
it(@"should have one view", ^{
OCMVerify(sdlLifecycleManager);
-
+
int expectedCount = 1;
expect(sentHapticRequest.hapticRectData.count).to(equal(expectedCount));
-
+
if(sentHapticRequest.hapticRectData.count == expectedCount) {
NSArray<SDLHapticRect *> *hapticRectData = sentHapticRequest.hapticRectData;
SDLHapticRect *sdlhapticRect = hapticRectData[0];
SDLRectangle *sdlRect = sdlhapticRect.rect;
-
+
compareRectangle(sdlRect, viewRect1);
}
});
});
-
+
context(@"when initialized with no views and then updated with two additional views", ^{
beforeEach(^{
hapticManager = [[SDLFocusableItemLocator alloc] initWithViewController:uiViewController connectionManager:sdlLifecycleManager];
hapticManager.enableHapticDataRequests = YES;
[hapticManager updateInterfaceLayout];
-
+
viewRect1 = CGRectMake(101, 101, 50, 50);
UITextField *textField1 = [[UITextField alloc] initWithFrame:viewRect1];
[uiViewController.view addSubview:textField1];
-
+
viewRect2 = CGRectMake(201, 201, 50, 50);
UITextField *textField2 = [[UITextField alloc] initWithFrame:viewRect2];
[uiViewController.view addSubview:textField2];
-
+
[hapticManager updateInterfaceLayout];
});
-
+
it(@"should have two views", ^{
OCMVerify(sdlLifecycleManager);
-
+
int expectedCount = 2;
expect(sentHapticRequest.hapticRectData.count).to(equal(expectedCount));
-
+
if(sentHapticRequest.hapticRectData.count == expectedCount) {
NSArray<SDLHapticRect *> *hapticRectData = sentHapticRequest.hapticRectData;
SDLHapticRect *sdlhapticRect1 = hapticRectData[0];
SDLRectangle *sdlRect1 = sdlhapticRect1.rect;
-
+
SDLHapticRect *sdlhapticRect2 = hapticRectData[1];
SDLRectangle *sdlRect2 = sdlhapticRect2.rect;
-
+
compareRectangle(sdlRect1, viewRect2);
compareRectangle(sdlRect2, viewRect1);
}
});
});
-
+
context(@"when initialized with nested views", ^{
beforeEach(^{
UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(101, 101, 50, 50)];
[uiViewController.view addSubview:textField];
-
+
viewRect1 = CGRectMake(110, 110, 10, 10);
UITextField *textField1 = [[UITextField alloc] initWithFrame:viewRect1];
[textField addSubview:textField1];
-
+
viewRect2 = CGRectMake(130, 130, 10, 10);
UITextField *textField2 = [[UITextField alloc] initWithFrame:viewRect2];
[textField addSubview:textField2];
-
+
hapticManager = [[SDLFocusableItemLocator alloc] initWithViewController:uiViewController connectionManager:sdlLifecycleManager];
hapticManager.enableHapticDataRequests = YES;
[hapticManager updateInterfaceLayout];
});
-
+
it(@"should have only leaf views added", ^{
OCMVerify(sdlLifecycleManager);
-
+
int expectedCount = 2;
expect(sentHapticRequest.hapticRectData.count).to(equal(expectedCount));
-
+
if(sentHapticRequest.hapticRectData.count == expectedCount) {
NSArray<SDLHapticRect *> *hapticRectData = sentHapticRequest.hapticRectData;
SDLHapticRect *sdlhapticRect1 = hapticRectData[0];
SDLRectangle *sdlRect1 = sdlhapticRect1.rect;
-
+
SDLHapticRect *sdlhapticRect2 = hapticRectData[1];
SDLRectangle *sdlRect2 = sdlhapticRect2.rect;
-
+
compareRectangle(sdlRect1, viewRect1);
compareRectangle(sdlRect2, viewRect2);
}
});
});
-
+
context(@"when initialized with nested button views", ^{
beforeEach(^{
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(101, 101, 50, 50)];
[uiViewController.view addSubview:button];
-
+
viewRect1 = CGRectMake(110, 110, 10, 10);
UIButton *button1 = [[UIButton alloc] initWithFrame:viewRect1];
[button addSubview:button1];
-
+
viewRect2 = CGRectMake(130, 130, 10, 10);
UITextField *textField2 = [[UITextField alloc] initWithFrame:viewRect2];
[button addSubview:textField2];
-
+
hapticManager = [[SDLFocusableItemLocator alloc] initWithViewController:uiViewController connectionManager:sdlLifecycleManager];
hapticManager.enableHapticDataRequests = YES;
[hapticManager updateInterfaceLayout];
});
-
+
it(@"should have only leaf views added", ^{
OCMVerify(sdlLifecycleManager);
-
+
int expectedCount = 2;
expect(sentHapticRequest.hapticRectData.count).to(equal(expectedCount));
-
+
if(sentHapticRequest.hapticRectData.count == expectedCount) {
NSArray<SDLHapticRect *> *hapticRectData = sentHapticRequest.hapticRectData;
SDLHapticRect *sdlhapticRect1 = hapticRectData[0];
SDLRectangle *sdlRect1 = sdlhapticRect1.rect;
-
+
SDLHapticRect *sdlhapticRect2 = hapticRectData[1];
SDLRectangle *sdlRect2 = sdlhapticRect2.rect;
-
+
compareRectangle(sdlRect1, viewRect1);
compareRectangle(sdlRect2, viewRect2);
}
});
});
-
+
context(@"when initialized with two views and then updated with one view removed", ^{
beforeEach(^{
viewRect1 = CGRectMake(101, 101, 50, 50);
UITextField *textField1 = [[UITextField alloc] initWithFrame:viewRect1];
[uiViewController.view addSubview:textField1];
-
+
viewRect2 = CGRectMake(201, 201, 50, 50);
UITextField *textField2 = [[UITextField alloc] initWithFrame:viewRect2];
[uiViewController.view addSubview:textField2];
-
+
hapticManager = [[SDLFocusableItemLocator alloc] initWithViewController:uiViewController connectionManager:sdlLifecycleManager];
hapticManager.enableHapticDataRequests = YES;
[hapticManager updateInterfaceLayout];
-
+
[textField2 removeFromSuperview];
-
+
[hapticManager updateInterfaceLayout];
});
-
+
it(@"should have one view", ^{
OCMVerify(sdlLifecycleManager);
-
+
int expectedCount = 1;
expect(sentHapticRequest.hapticRectData.count).to(equal(expectedCount));
-
+
if(sentHapticRequest.hapticRectData.count == expectedCount) {
NSArray<SDLHapticRect *> *hapticRectData = sentHapticRequest.hapticRectData;
SDLHapticRect *sdlhapticRect = hapticRectData[0];
SDLRectangle *sdlRect = sdlhapticRect.rect;
-
+
compareRectangle(sdlRect, viewRect1);
}
});
@@ -295,51 +295,51 @@ describe(@"the haptic manager", ^{
viewRect1 = CGRectMake(101, 101, 50, 50);
UITextField *textField1 = [[UITextField alloc] initWithFrame:viewRect1];
[uiViewController.view addSubview:textField1];
-
+
hapticManager = [[SDLFocusableItemLocator alloc] initWithViewController:uiViewController connectionManager:sdlLifecycleManager];
hapticManager.enableHapticDataRequests = YES;
[hapticManager updateInterfaceLayout];
-
+
viewRect2 = CGRectMake(201, 201, 50, 50);
UITextField *textField2 = [[UITextField alloc] initWithFrame:viewRect2];
[uiViewController.view addSubview:textField2];
-
+
[[NSNotificationCenter defaultCenter] postNotificationName:SDLDidUpdateProjectionView object:nil];
});
-
+
it(@"should have two views", ^{
OCMVerify(sdlLifecycleManager);
-
+
int expectedCount = 2;
expect(sentHapticRequest.hapticRectData.count).to(equal(expectedCount));
-
+
if(sentHapticRequest.hapticRectData.count == expectedCount) {
NSArray<SDLHapticRect *> *hapticRectData = sentHapticRequest.hapticRectData;
SDLHapticRect *sdlhapticRect1 = hapticRectData[0];
SDLRectangle *sdlRect1 = sdlhapticRect1.rect;
-
+
SDLHapticRect *sdlhapticRect2 = hapticRectData[1];
SDLRectangle *sdlRect2 = sdlhapticRect2.rect;
-
+
compareRectangle(sdlRect1, viewRect2);
compareRectangle(sdlRect2, viewRect1);
}
});
});
-
+
context(@"when touched inside a view", ^{
beforeEach(^{
UITextField *textField1 = [[UITextField alloc] initWithFrame:CGRectMake(101, 101, 50, 50)];
[uiViewController.view addSubview:textField1];
-
+
UITextField *textField2 = [[UITextField alloc] initWithFrame:CGRectMake(201, 201, 50, 50)];
[uiViewController.view addSubview:textField2];
-
+
hapticManager = [[SDLFocusableItemLocator alloc] initWithViewController:uiViewController connectionManager:sdlLifecycleManager];
hapticManager.enableHapticDataRequests = YES;
[hapticManager updateInterfaceLayout];
});
-
+
it(@"should return a view object", ^{
UIView *view1 = [hapticManager viewForPoint:CGPointMake(125, 120)];
expect(view1).toNot(beNil());
@@ -348,31 +348,31 @@ describe(@"the haptic manager", ^{
expect(view2).toNot(beNil());
});
});
-
+
context(@"when touched in overlapping views' area", ^{
beforeEach(^{
UITextField *textField1 = [[UITextField alloc] initWithFrame:CGRectMake(101, 101, 50, 50)];
[uiViewController.view addSubview:textField1];
-
+
UITextField *textField2 = [[UITextField alloc] initWithFrame:CGRectMake(126, 126, 50, 50)];
[uiViewController.view addSubview:textField2];
-
+
hapticManager = [[SDLFocusableItemLocator alloc] initWithViewController:uiViewController connectionManager:sdlLifecycleManager];
hapticManager.enableHapticDataRequests = YES;
[hapticManager updateInterfaceLayout];
});
-
+
it(@"should return no view object", ^{
UIView* view = [hapticManager viewForPoint:CGPointMake(130, 130)];
expect(view).to(beNil());
});
});
-
+
context(@"when touched outside view boundary", ^{
beforeEach(^{
UITextField *textField1 = [[UITextField alloc] initWithFrame:CGRectMake(101, 101, 50, 50)];
[uiWindow insertSubview:textField1 aboveSubview:uiWindow];
-
+
hapticManager = [[SDLFocusableItemLocator alloc] initWithViewController:uiViewController connectionManager:sdlLifecycleManager];
hapticManager.enableHapticDataRequests = YES;
[hapticManager updateInterfaceLayout];
@@ -381,7 +381,7 @@ describe(@"the haptic manager", ^{
UIView* view = [hapticManager viewForPoint:CGPointMake(0, 228)];
expect(view).to(beNil());
});
-
+
});
});