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 @@ - (void)sdl_sendHapticRPC { } NSMutableArray *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 @@ - (void)sdl_sendHapticRPC { 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 @@ - (void)sdl_sendHapticRPC { #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 @@ - (nullable UIView *)viewForPoint:(CGPoint)point { } } } - + 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 @@ - (void)sdl_handleTouchBegan:(SDLTouch *)touch { 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 @@ - (void)sdl_handleTouchMoved:(SDLTouch *)touch { 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 @@ - (void)sdl_handleTouchMoved:(SDLTouch *)touch { _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 @@ - (void)sdl_handleTouchEnded:(SDLTouch *)touch { [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 @@ - (void)sdl_handleTouchEnded:(SDLTouch *)touch { } 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 @@ - (void)sdl_handleTouchEnded:(SDLTouch *)touch { 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,12 +420,36 @@ - (void)sdl_initializeSingleTapTimerAtPoint:(CGPoint)point { 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 */ 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 @@ BOOL compareRectangle(SDLRectangle *sdlRectangle, CGRect cgRect) { 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 @@ BOOL compareRectangle(SDLRectangle *sdlRectangle, CGRect cgRect) { 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 *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 *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 *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 *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 *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 *hapticRectData = sentHapticRequest.hapticRectData; SDLHapticRect *sdlhapticRect = hapticRectData[0]; SDLRectangle *sdlRect = sdlhapticRect.rect; - + compareRectangle(sdlRect, viewRect1); } }); @@ -295,51 +295,51 @@ BOOL compareRectangle(SDLRectangle *sdlRectangle, CGRect cgRect) { 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 *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 @@ BOOL compareRectangle(SDLRectangle *sdlRectangle, CGRect cgRect) { 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 @@ BOOL compareRectangle(SDLRectangle *sdlRectangle, CGRect cgRect) { UIView* view = [hapticManager viewForPoint:CGPointMake(0, 228)]; expect(view).to(beNil()); }); - + }); });