I'm trying to override hitTest:withEvent in a custom view. This is a container view with many subviews.
Attached to this "superView" is a gesture recognizer. On top of this view sit many subviews (which in turn have more and more subviews) I decided to over ride hitTesh:withEvent in order to decide which view should get the events - and in which cases the superview should get the event in order to trigger the gesture.
Problem I am having is that hitTest will return the superview in cases where a tap gesture would be initialized in a subview. In that case I would like to "retroactively" disregard the tap action - and again "invoke" hitTest:withEvent on the superview. the superview could then decide to pass the "true" responder to this event.
How can I do it:
Example:
// In the container view
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
// the default view that would have been returned
UIView *v = [super hitTest:point withEvent:event];
// if nil than touch wasn't in this view so disregard
if (v == nil)
{
return nil;
}
// if the touch was on a control than I don't want to take care of it
if ([v isKindOfClass:[UIControl Class]])
{
return v;
}
// now here is where I need to fix
// I am only interested in a touch event that would initiated a pan gesture
// if the event was a tap for intance - than return [super hitTest:point withEvent:event]
// for instance if a uitableview cell was tapped - I don't want to mask the event
if (event wouldn't initiate a pan)
{
return v;
}
return self;
}
Related
I have a custom subclass of MKPinAnnotationView that displays a custom call out. I want to handle touch events inside that annotation.
I have a working solution (below) but it just doesn't feel right. I have a rule of thumb that whenever I use performSelector: .. withDelay: I'm fighting the system rather than working with it.
Does anyone have a good, clean workaround for the aggressive event handling of MKMapView and annotation selection handling?
My current solution:
(All code from my annotation selection class)
I do my own hit testing (without this my gesture recognisers don't fire as the Map View consumes the events:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event; {
// To enable our gesture recogniser to fire. we have to hit test and return the correct view for events inside the callout.
UIView* hitView = nil;
if (self.selected) {
// check if we tpped inside the custom view
if (CGRectContainsPoint(self.customView.frame, point))
hitView = self.customView;
}
if(hitView) {
// If we are performing a gesture recogniser (and hence want to consume the action)
// we need to turn off selections for the annotation temporarily
// the are re-enabled in the gesture recogniser.
self.selectionEnabled = NO;
// *1* The re-enable selection a moment later
[self performSelector:#selector(enableAnnotationSelection) withObject:nil afterDelay:kAnnotationSelectionDelay];
} else {
// We didn't hit test so pass up the chain.
hitView = [super hitTest:point withEvent:event];
}
return hitView;
}
Note that I also turn off selections so that in my overridden setSelected I can ignore the deselection.
- (void)setSelected:(BOOL)selected animated:(BOOL)animated; {
// If we have hit tested positive for one of our views with a gesture recogniser, temporarily
// disable selections through _selectionEnabled
if(!_selectionEnabled){
// Note that from here one, we are out of sync with the enclosing map view
// we're just displaying out callout even though it thinks we've been deselected
return;
}
if(selected) {
// deleted code to set up view here
[self addSubview:_customView];
_mainTapGestureRecogniser = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(calloutTapped:)];
[_customView addGestureRecognizer: _mainTapGestureRecogniser];
} else {
self.selected = NO;
[_customView removeFromSuperview];
}
}
It's the line commented 1 that I don't like but it's also pretty hairy at the end of theta delayed fire. I have to walk back up the superview chain to get to the mapView so that I can convince it the selection is still in place.
// Locate the mapview so that we can ensure it has the correct annotation selection state after we have ignored selections.
- (void)enableAnnotationSelection {
// This reenables the seelction state and resets the parent map view's idea of the
// correct selection i.e. us.
MKMapView* map = [self findMapView];
[map selectAnnotation:self.annotation animated:NO];
_selectionEnabled = YES;
}
with
-(MKMapView*)findMapView; {
UIView* view = [self superview];
while(!_mapView) {
if([view isKindOfClass:[MKMapView class]]) {
_mapView = (MKMapView*)view;
} else if ([view isKindOfClass:[UIWindow class]]){
return nil;
} else{
view = [view superview];
if(!view)
return nil;
}
}
return _mapView;
}
This all seems to work without and downside (like flicker I've seen from other solutions. It's relatively straightforward but it doesn't feel right.
Anyone have a better solution?
I don't think you need to monkey with the map view's selection tracking. If I'm correctly interpreting what you want, you should be able to accomplish it with just the hitTest:withEvent: override and canShowCallout set to NO. In setSelected: perform your callout appearance/disappearance animation accordingly. You should also override setHighlighted: and adjust the display of your custom callout if visible.
In an single view iPhone app I have the following structure: scrollView -> contentView -> imageView and underneath (at the main view) there are 7 draggable custom views - which can be dragged from/to the contenView.
Initially dragging was handled in the custom views (see Tile.m#2eb672523a) and while this worked well it required sending notifications to the main view.
So I'm trying to move touchesBegan and friends to the main app view (see ViewController.m).
My problem is:
Initially I had this code in the contentView (see GameBoard.m) to enable subview dragging at the scrollView:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
return result == self ? nil : result;
}
However when trying to drag a tile back from contentView to the main view (in the direction shown in the below screenshot) this wouldn't deliver touchesBegan to the main view.
So I have changed this method to return the main view instead of the tiles:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
return result == self ? nil : self.window.rootViewController.view;
}
Now touchesBegan is called on the main view (as I can see in the debugger), but the touches.view is pointing to the main view too.
My question is:
How to find the touched tile in the new touchesBegan method?
Nevermind, I've solved my problem.
In ViewController.m I've added a _draggedTile ivar and set it in touchesBegan by using the following method to find the touched tile:
- (SmallTile*) findTileAtPoint:(CGPoint)point withEvent:(UIEvent*)event
{
NSArray* children = [self.view.subviews arrayByAddingObjectsFromArray:_contentView.subviews];
for (UIView* child in children) {
CGPoint localPoint = [child convertPoint:point fromView:self.view];
if ([child isKindOfClass:[SmallTile class]] &&
[child pointInside:localPoint withEvent:event]) {
return (SmallTile*)child;
}
}
return nil;
}
And in touchedMoved, touchesEnded, touchesCancelled I just use that ivar (initially I was using the above findTileAtPoint:withEvent: method there as well, but sometimes the touch wouldn't be exactly over the dragged tile and the movement was jerky or cancelled).
I have a clear UIView which has gesture recognizers attached to it.
This clear uiview covers the entire super view to allow for the gestures to be invoked from anywhere on it.
Under this clear UIView sit different components such as tables,buttons,collectionview etc.
The clear UIView has no idea what is under it any time.
What I want - if a view which is under the clear uiview can handle a touch event (or any type of gesture) - the clear view should disregard that event - and the event will pass through to the underlying view which could handle it.
I tried
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
but I don't know how to make sure the underlying view can handle it.
-(id)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
id hitView = [super hitTest:point withEvent:event];
if (hitView == self)
{
return nil;
}
else
{
return hitView;
}
}
Add this to your to clear view.
If the hit on clear view means just return nil.
You can override pointInside: withEvent: method. This method returns a boolean value indicating whether the receiver contains the specified point. So if we return NO then your upper clear view will become transparent for touch events and they will be passed to underlying views.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// Clear UIView will now respond to touch events if return NO:
return NO;
}
use below code for your case->
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *hitTestView = [super hitTest:point withEvent:event];
if(hitTestView!=nil){
//check for gesture
if([hitTestView.gestureRecognizers count]>0)
return hitTestView;
//if it is subclass of UIControl like UIButton etc
else if([hitTestView isKindOfClass:[UIControl class]])
return hitTestView;
//if can handle touches
else if([hitTestView respondsToSelector:#selector(touchesBegan:withEvent:)])
return hitTestView;
else
return nil;
}
else{
return self;
}
}
In the above code if the subView which is hitView can anyway handle touch ,we return that object to handle that touch. If there is no such hitTest view, then we return the view itself.
I used some of these suggestions and used the following solution:
I added the gesture recognizer to the bottom most superview in the heirarchy (and not the top most)
Then in that class over rid
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *v = [super hitTest:point withEvent:event];
// if v is nil then touch wasn't in this view or its subviews
if (v == nil)
{
return nil;
}
// in any case if the topview was hidden than return the default value
if (self.myTopView.hidden)
{
return v;
}
// if the view isn't hidden but the touch returned a control - than we can pass the touch to the control
if ([v isKindOfClass:[UIControl class]])
{
return v;
}
// decide on what threshold to decide is a touch
CGFloat threshHold = 40;
// if the touch wasn't on a control but could initiate a gesture than that view should get the touch
if (v.gestureRecognizers)
{
threshHold = 30;
// return v;
}
// check if the threshold should be bigger
if ([self someCondition])
{
threshHold = 100;
}
// threshold according to its position - this is the dynamic part
if (point.y > (self.myTopView.frame.origin.y - threshold))
{
return self.handleBarView;
}
return v;
}
Problem: Using NIAttributedLabel in UITableViewCell with Action(tap, navigate)
adding a link in label
adding label to cell
adding cell to model with action tap
Here is the problem, if I touch the label on the link, it actually does not show the link but act the tap action.
But if I add a UIButton in UITableViewCell in the same way, the action does not happen and the button response when I touch on the button.
So I guess it is the problem with the label.
How can I solve it?
I figured out this finally;
adding function to file NIAttributedLabel.m
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// never return self. always return the result of [super hitTest..].
// this takes userInteraction state, enabled, alpha values etc. into account
UIView *hitResult = [super hitTest:point withEvent:event];
// don't check for links if the event was handled by one of the subviews
if (hitResult != self) {
return hitResult;
}
if (self.explicitLinkLocations || self.detectedlinkLocations) {
BOOL didHitLink = ([self linkAtPoint:point] != nil);
if (!didHitLink) {
// not catch the touch if it didn't hit a link
return nil;
}
}
return hitResult;
}
remove all [super touch XXXX] functions in all touchXXX;
then, it works!
This question already has answers here:
UIScrollView touchesBegan
(8 answers)
Closed 9 years ago.
I have subclassed UIScrollView and implemented
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
method in it.
But Nothing happens when click or hold the scroll view!
Edit: I also need to use touchesEnded method
I think UIScrollView has two gesture recognizer. They are responsible for handling touch sequences, so they swallow the touch events.
Use scrollView delegate methods to handle drag gestures or the
touchesShouldBegin:withEvent:inContentView:
method to handle scrollview content touches and
touchesShouldCancelInContentView:
to cancel it.
As alternative you can manipulate the panGesture recognizer of the scrollView to pass the event to the next responser.
In your subclass of UIScrollView override the hitTest method like this:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *result = nil;
for (UIView *child in self.subviews)
if ([child pointInside:point withEvent:event])
if ((result = [child hitTest:point withEvent:event]) != nil)
break;
return result;
}
I think you are using scrollview as a subview.So in that case you can use gesture coz same problem i've face it.
You can do using UITapGestureRecognizer like this ...
-(void)viewDidLoad
{
UITapGestureRecognizer *gr = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
gr.delegate = self;
[self.scrollView addGestureRecognizer:gr];
}
- (void)handleGesture:(UIGestureRecognizer *)gestureRecognizer
{
// do here whatever you wanna ..........
}
Solved!
Used LongPressedGestureRecognizer. The action method keeps getting called till the user holds the view. From that i can figure out the state of the gestureRecognizer (UIGestureRecognizerStateBegan, ...Ended, ...Cancelled)