How to disable didSelectRowAtIndexPath on UIView Tap inside custom TableViewCell? - ios

I have a UITableView with custom TableViewCell inside that custom UITableViewCell there is a UIView(myView). I want to disable didSelectRowAtIndexPath when I tap on myView,didSelectRowAtIndexPathshould not be called when I tap on myView.
it's not same as this question because in that question it's not clear what the user want to achieve and also answer is different.

You can override the pointInside of UITableViewCell, try this:
// MyCell.m
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
BOOL pointInside = [super pointInside:point withEvent:event];
if (pointInside && ![self.myView pointInside:[self convertPoint:point toView:self.myView] withEvent:event]) {
return YES;
}
return NO;
}
didSelectRowAtIndexPathshould not be called when u tap on myView

Accodding to the Responder Chain say
iOS uses hit-testing to find the view that is under a touch. Hit-testing involves checking whether a touch is within the bounds of any relevant view objects. If it is, it recursively checks all of that view’s subviews. The lowest view in the view hierarchy that contains the touch point becomes the hit-test view. After iOS determines the hit-test view, it passes the touch event to that view for handling.
In the image blow :
(Figure 2-1 Hit-testing returns the subview that was touched)
(source: apple.com)
To illustrate, suppose that the user touches view E in Figure 2-1. iOS finds the hit-test view by checking the subviews in this order:
The touch is within the bounds of view A, so it checks subviews B
and C.
The touch is not within the bounds of view B, but it’s within the
bounds of view C, so it checks subviews D and E.
The touch is not within the bounds of view D, but it’s within the
bounds of view E. View E is the lowest view in the view hierarchy
that contains the touch, so it becomes the hit-test view.
As most app do,there is no need to disable the execution of didSelectRowAtIndexPath function.What you can do here ,is to add a UIButton or add a UITapGesture on your myView and perform the desired task,when you touch in the area of the UITapGesture, didSelectRowAtIndexPath will not execute.
Just like this:

Related

UITableView inside UIScrollView not receiving first tap after scrollling

Brief
I am having an issue with a UITableView inside a UIScrollView. When I scroll the external scrollView, the table does not receive the willSelect/didSelect event on the first touch, but it does on the second one. What is even more strange, the cell itself gets the touches and the highlighted state, even when the delegate does not.
Detailed explanation
My view hierarchy:
UIView
- UIScrollView (outerscroll)
- Some other views and buttons
- UITableView (tableView)
Inside the scroll view I have some extra views that get expanded/closed dynamically. The table view needs to get "fixed" on top, together with some other elements of the view, so that is why I created this layout, that allows me to easily move elements in a similar way than Apple recommends by the use of transformations when the scroll happens.
The table View is transformed with a translation effect when the outerscroll moves like this:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.outerScrollView) {
CGFloat tableOffset = scrollView.contentOffset.y - self.fixedHeaderFrame.origin.y;
if (tableOffset > 0) {
self.tableView.contentOffset = CGPointMake(0, tableOffset);
self.tableView.transform = CGAffineTransformMakeTranslation(0, tableOffset);
}
else {
self.tableView.contentOffset = CGPointMake(0, 0);
self.tableView.transform = CGAffineTransformIdentity;
}
// Other similar transformations are done here, but not involving the table
}
In my cell, if I implement these methods:
- (void)setSelected:(BOOL)selected {
[super setSelected:selected];
if (selected) {
NSLog(#"selected");
}
}
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
{
[super setHighlighted:highlighted animated:animated];
if (highlighted) {
NSLog(#"highlighted");
}
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
NSLog(#"touchesBegan");
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
NSLog(#"touchesEnded");
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
NSLog(#"touchesCancelled");
}
Y can see this output when fails (first tap):
2014-02-10 13:04:40.940 MyOrderApp[5588:70b] highlighted
2014-02-10 13:04:40.940 MyOrderApp[5588:70b] touchesBegan
2014-02-10 13:04:40.978 MyOrderApp[5588:70b] touchesEnded
And this one when works (second tap):
2014-02-10 13:05:30.359 MyOrderApp[5588:70b] highlighted
2014-02-10 13:05:30.360 MyOrderApp[5588:70b] touchesBegan
2014-02-10 13:05:30.487 MyOrderApp[5588:70b] touchesEnded
2014-02-10 13:05:30.498 MyOrderApp[5588:70b] expanded
No other frame change, animation or any other view interaction is done between the first and the second tap. Also, only when scrolling large amounts the bug appears, but with scrollings of just a few pixels everything keeps working as expected.
I experimented changing some properties as well, but with no luck. Some of the things I did:
Remove userInteractionEnabled from views other than the scroll and table
Add a call to setNeedsLayout on the table, scroll and main view when scrollViewDidScroll occurs.
Remove the transformations from the table (still happens)
I have seen some comments about the unexpected behaviour of embedding UITableViews inside UIScrollViews but I can not see such a warn in the official documentation by Apple, so I am expecting it to work.
The app is iOS7+ only.
Questions
Has anyone experienced similar issues? Why is this and how can I solve it? I think that I could be able to intercept the tap gesture on the cell and pass it with a custom delegate or similar, but I would like the table to receive the proper events and so my UITableViewDelegate receives it as expected.
Updates
I tried disabling cell reuse as suggested in a comment but it still happens in the same way.
leave the inner UITableView's scrollEnabled property set as YES. this lets the inner UITableView know to handle scroll-related touches on the UIScrollView correctly.
From Apple Documentation, you shouldn't embed a UITableViewinside a UIScrollView.
Important: You should not embed UIWebView or UITableView objects in
UIScrollView objects. If you do so, unexpected behavior can result
because touch events for the two objects can be mixed up and wrongly
handled.
Your problem is really related to what your UIScrollView does.
But if it's just to hide the tableview when needed (that was my case), you can just move the UITableView in its superview.
I wrote a small example here : https://github.com/rvirin/SoundCloud/
I ran into this same problem and figured out a solution!!
You need to set the delaysTouchesBegan to true on your scrollview so that the scrollview sends its failed scrolled-gesture (i.e. the tap) to its children.
var delaysTouchesBegan: Bool -
A Boolean value determining whether the receiver delays sending touches in a begin phase to its view.
When the value of the property is YES, the window suspends delivery of
touch objects in the UITouchPhaseBegan phase to the view. If the
gesture recognizer subsequently recognizes its gesture, these touch
objects are discarded. If the gesture recognizer, however, does not
recognize its gesture, the window delivers these objects to the view
in a touchesBegan:withEvent: message (and possibly a follow-up
touchesMoved:withEvent: message to inform it of the touches’ current
locations).
https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIGestureRecognizer_Class/index.html#//apple_ref/occ/instp/UIGestureRecognizer/delaysTouchesBegan
But there's a catch...it doesn't work if you do it directly on the scrollview!
// Does NOT work
self.myScrollview.delaysTouchesBegan = true
Apparently this is an iOS bug where setting this property doesn't work (thank you apple). However there's a simple workaround: set the property directly on the scrollview's pan gesture. Sure enough, this worked for me perfectly:
// This works!!
self.myScrollview.panGestureRecognizer.delaysTouchesBegan = true
It seems that your UiTableView doesn't recognize your tap. Did you try to use that :
- (BOOL)gestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UISwipeGestureRecognizer *)otherGestureRecognizer
{
if ([otherGestureRecognizer.view isKindOfClass:[UITableView class]]) {
return YES;
}
return NO;
}
Note from apple:
called when the recognition of one of gestureRecognizer or otherGestureRecognizer would be blocked by the other. return YES to allow both to recognize simultaneously. the default implementation returns NO (by default no two gestures can be recognized simultaneously)
note: returning YES is guaranteed to allow simultaneous recognition. returning NO is not guaranteed to prevent simultaneous recognition, as the other gesture's delegate may return YES
Hope that will help.
Gesture recognizers won't work correctly for two embedded scroll views or subclasses.
Try a workaround:
Use transparent, custom, and overlaying everything in cell UIButton with proper tag, or subclass UIButton and add a index path property and overwrite each time in reused cell.
Add this button as a property to your custom cell.
Add target for desired UIControlEvent (one or more) that points to your UITableViewDelegate protocol adopting class.
Disable selecting in IB, and manually manage the selection from code.
This solution requires attention for cases of single/multi selection.
I've encountered a UITableView with scrollEnabled being NO within a UIScrollView in some legacy code. I have not been able to change the existing hierarchy easily nor enable scrolling, but come up with the the following workaround for the first tap problem:
#interface YourOwnTableView : UITableView
#end
#implementation YourOwnTableView
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
// Note that this is a hack and it can stop working at some point.
// Looks like a table view with scrollEnabled being NO does not handle cancellation cleanly,
// so let's repeat begin/end touch sequence here hoping it'll reset its own internal state properly
// but won't trigger cell selection (the touch passed is in its cancelled phase, perhaps there is a part
// of code inside which actually checks it)
[super touchesBegan:touches withEvent:event];
[super touchesEnded:touches withEvent:event];
}
#end
Again, this is just a workaround working in my specific case. Having a table view within a scroll view is still a wrong thing.
I would recommend to look for options like not letting your cell to be in highlighted state when you are actually scrolling the outer scroll view which is very easy to handle and is the recommended way. You can do this just by taking a boolean and toggling it in the below method
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
The scrollview is trying to figure out whether the user's intention is to scroll or not, so it's delaying the initial touch on purpose. You can turn this off by setting delaysContentTouches to NO.
I have the same problem with nested UITableView and have found a work-around for this:
innerTableView.scrollEnabled = YES;
innerTableView.alwaysBounceVertical = NO;
You'll need to set the height of the inner table view to match with the total height of its cells so that it'll not scroll when user scrolling the outer view.
Hope this helps.
My mistake was implementing the delegate method:
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath
instead of the one I meant to implement:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
Hence only being called on the second cell being tapped, because that was when the first cell would be de selected. Stupid mistake made with the help of autocomplete. Just a thought for those of you who may wander here not realizing you've made the same mistake too.
Drop a UIButton over your UITableViewCell and create the outlet as "btnRowSelect".
In your view controller put this code in cellForRowAtIndexPath
cell.btnRowSelect.tag = indexPath.row
cell.btnRowSelect.addTarget(self, action: Selector("rowSelect:"), forControlEvents: .TouchUpInside)
Add this function to your viewController as well-
func rowSelect (sender:UIButton) {
// "sendet.tag" give you the selected row
// do whatever you want to do in didSelectRowAtIndexPath
}
This function "rowSelect" will work as didSelectRowAtIndexPath where
you get the row"indexPath.row" as "sender.tag"
As other answers say you shouldn't put a tableview in a scrollview. A UITableView inherits from UIScrollView anyway so I guess that's where things get confusing. What I always do in this situation is:
1) Subclass UITableViewController and include a property UIView *headView.
2) In the parent UIViewController create all the top stuff in a container UIView
3) Initialise your custom UITableView and add the tableView's view to the view controller full size
[self.view addSubview: self.myTableView.view];
4) Set the headView to be your UIView gubbins
self.tableView.headView = myHeadViewGubbins.
5) In the tableViewController method
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger *)section;
Do:
if ( section == 0 ) {
return self.headView;
}
Now you have a table view with a bunch of other shizzle at the top.
Enjoy!
That it, if touch table view it will work properly. also with scroll view in same view controller also.
tableview.scrollEnabled = true;
I have the same issue, Then refer to "Nesting Scroll Views" as lxx said.
https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/UIScrollView_pg/NestedScrollViews/NestedScrollViews.html
An example of cross directional scrolling can be found in the Stocks application. The top view is a table view, but the bottom view is a horizontal scroll view configured using paging mode. While two of its three subviews are custom views, the third view (that contains the news articles) is a UITableView (a subclass of UIScrollView) that is a subview of the horizontal scroll view. After you scroll horizontally to the news view, you can then scroll its contents vertically.
It is work

iPad: How can I click buttons underneath transparent portions of a UIView

I am subclassing a view which is the same size as my main ViewController (1024x768). This subview has a transparent background and contains buttons that are sized 50w X 50h and are positioned dynamically.
My issue is that I need to interact with content and buttons that exist beneath this view but this subview is blocking that interaction.
I've seen some posts address a similar problem, but I am unclear of the actual usage.
-pointInside:withEvent: is how iOS asks if a touch is within a particular view. If a view returns YES, iOS calls -hitTest:withEvent: to determine the particular subview of that view that was touched. That method will return self if there are no subviews at the location of that touch. So you can pass any touches that aren't on subviews back to views behind this one by implementing -pointInside:withEvent: like this:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
return ([self hitTest:point withEvent:event] != self);
}
If you need to catch some touches that aren't on subviews, your implementation will be more complicated, but this method is still the right place to tell iOS where your view is and accepts touch events.
Did you try to set userInteractionEnabled to YES or NO?
If all else fails you can bring those subviews to the front programmatically using
[self.view bringSubviewToFront:buttonToClick];

How can I set up gesture recognizer to interact any UIView when all the views are being animated?

I found the code listed below from https://github.com/DuncanMC/iOS-CAAnimation-group-demo. This particular method allows the user to stop a UIView while it is "in-flight" in a core animation sequence using a gesture recognizer. When the view is tapped, the animation stops. As shown this code will only work on animated view. I have many animated views and I need interaction with any of the views. I think I must set up an array of views (or layers) and cycle through them. Is this correct? How could I do this? Thanks!
/*
This method gets called from a tap gesture recognizer installed on the view myContainerView.
We get the coordinates of the tap from the gesture recognizer and use it to hit-test
myContainerView.layer.presentationLayer to see if the user tapped on the moving image view's
(presentation) layer. The presentation layer's properties are updated as the animation runs, so hit-testing
the presentation layer lets you do tap and/or collision tests on the "in flight" animation.
*/
- (IBAction)testViewTapped:(id)sender
{
CALayer *tappedLayer;
id layerDelegate;
UITapGestureRecognizer *theTapper = (UITapGestureRecognizer *)sender;
CGPoint touchPoint = [theTapper locationInView: myContainerView];
if (animationInFlight)
{
tappedLayer = [myContainerView.layer.presentationLayer hitTest: touchPoint];
layerDelegate = [tappedLayer delegate];
if (((layerDelegate == imageOne && !doingMaskAnimation)) ||
(layerDelegate == waretoLogoLarge && doingMaskAnimation))
{
if (myContainerView.layer.speed == 0)
[self resumeLayer: myContainerView.layer];
else
{
[self pauseLayer: myContainerView.layer];
//Also kill all the pending label changes that we set up using performSelector:withObject:afterDelay
[NSObject cancelPreviousPerformRequestsWithTarget: animationStepLabel];
}
}
}
}
LOL. That demo project is mine. The code is written to find the layer that was tapped, and then use the fact that for a layer that backs a UIView, the layer's delegate is the view itself.
At the point in the code where it finds the layerDelegate, you should make sure it isKindOfClass UIView, then use whatever method is appropriate to match your view with the views you've animated. You could use an IBOutletCollection to keep track of the views that you are animating, or manually create a mutable array and add view objects to it, use view tags, or whatever makes sense for your application.

touches methods not getting called on UIView placed inside a UIScrollView

I have a Custom Scroll View, subclassing UIScrollView. I have added a scroll view in my viewcontroller nib file and changed its class to CustomScrollView. Now, this custom scroll view (made from xib) is added as a subview on self.view.
In this scroll view, I have 3 text fields and 1 UIImageView(named signImageView) added from xib. On clicking UIImageView (added a TapGestureRecogniser), a UIView named signView is added on the custom scroll view. I want to allow User to sign on this view, So I have created a class Signature.m and .h, subclassing UIView and implemented the touches methods (touchesBegan, touchesMoved and touchesEnded) and initialised the signView as follows:
signView = [[Signature alloc]initWithFrame:signImageView.frame];
[customScrollView addSubview:signView];
But when I start signing on the signView, the view gets scrolled and hence the touches methods don't get called.
I have tried adding signView on self.view instead of custom scroll view, but in that case the view remains glued to a fixed position when I start scrolling. (Its frame remains fixed in this case)
Try setting canCancelContentTouches of the scrollView to NO and delaysContentTouches to YES.
EDIT:
I see that similiar question was answered here Drag & sweep with Cocoa on iPhone (the answer is exactly the same).
If the user tap-n-holds the signView (for about 0.3-0.5 seconds) then view's touchesBegan: method gets fired and all events from that moment on go to the signView until touchesEnded: is called.
If user quickly swipes trough the signView then UIScrollView takes over.
Since you already have UIView subclassed with touchesBegan: method implemented maybe you could somehow indicate to user that your app is prepared for him to sign ('green light' equivalent).
You could also use touchesEnded: to turn off this green light.
It might be better if you add signImageView as as subView of signView (instead of to customScrollView) and hide it when touchesBegan: is fired). You would add signView to customScrollview at the same place where you add signImageView in existing code instead.
With this you achieve that there is effectively only one subView on that place (for better touch-passing efficiency. And you could achieve that green light effect by un-hiding signImageView in touchesBegan:/touchesEnded:
If this app-behaviour (0.3-0.5s delay) is unacceptable then you'd also need to subclass UIScrollView. There Vignesh's method of overriding UIScrollView's touchesShouldBegin: could come to the rescue. There you could possibly detect if the touch accoured in signView and pass it to that view immediately.
When ever you add a scrollview in your view hierarchy it swallows all touches.Hence you are not getting the touches began. So to get the touches in your signon view you will have to pass the touches to signon view. This is how you do it.
We achieved this with a UIScrollView subclass that disables the pan gesture recogniser for a list of views that you provide.
class PanGestureSelectiveScrollView: UIScrollView {
var disablePanOnViews: [UIView]?
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let disablePanOnViews = disablePanOnViews else {
return super.gestureRecognizerShouldBegin(gestureRecognizer)
}
let touchPoint = gestureRecognizer.location(in: self)
let isTouchingAnyDisablingView = disablePanOnViews.first { $0.frame.contains(touchPoint) } != nil
if gestureRecognizer === panGestureRecognizer && isTouchingAnyDisablingView {
return false
}
return true
}
}

How do I make a MKAnnotationView touch sensitive?

I currently have a map view with some annotation on it. I have the annotation with custom images. The problem I am trying to fix is the sensitivity of the images. When I try to drag them, it feels like I have to touch the exact center for it to be focused on. Is there a way to make the touch bounds bigger?
To do this, you need to subclass MKAnnotationView to create your own custom MKAnnotationView. In your subclass, override the following functions:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
// Return YES if the point is inside an area you want to be touchable
}
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
// Return the deepest view that the point is inside of.
}
This allows interactive views (such as buttons, etc.) to be pressed. The default implementation in MKAnnotationView is not strict on pointInside and hitTest because it allows presses that are actually inside one annotation to get sent to a different annotation. It does this by figuring out the closest annotation center to the touch-point and sending the events to that annotation, this is so that close-together (overlapping) annotations don't block each other from being selected. However, in your case you probably want to block other annotations if the user is to select and drag the topmost annotation, so the above method is probably what you want, or else it will set you on the right path.
EDIT:
I was asked in comments for an example implementation of hitTest:withEvent: - This depends on exactly what you are trying to achieve. The original question suggested touching and dragging images within the annotation whereas in my case I have some buttons inside the annotation that I want to be interactive. Here's my code:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
UIView* hitView = [super hitTest:point withEvent:event];
if (hitView != nil)
{
// ensure that the callout appears above all other views
[self.superview bringSubviewToFront:self];
// we tapped inside the callout
if (CGRectContainsPoint(self.resultView.callButton.frame, point))
{
hitView = self.resultView.callButton;
}
else if (CGRectContainsPoint(self.resultView.addButton.frame, point))
{
hitView = self.resultView.addButton;
}
else
{
hitView = self.resultView.viewDetailsButton;
}
[self preventSelectionChange];
}
return hitView;
}
As you can see it's quite simple - The MKAnnotationView implementation (called as super on the first line of my implementation) only returns the first (outermost) view, it does not drill down through the view hierarchy to see which sub-view the touch is actually inside. In my case I just check if the touch is inside one of three buttons and return those. In other circumstances you may have simple rectangle-based drilling down through the hierarchy or more complex hit tests for example to avoid transparent areas within your view to allow touches to pass through those parts. If you do need to drill down, CGRectContainsPoint can be used the same way I have used it, but remember to offset your points into local view coordinates for each view-level you drill into.
The preventSelectionChange method is to prevent my custom annotation from becoming selected, I am using it as a customisable/interactive callout from map pins and this keeps the pin it relates to selected instead of allowing the selection to change to this annotation.
Did you subclass MKAnnotationView or did you change the just set the image property of it?
Here's the documentation for setting the image property.
Discussion:
Assigning a new image to this property also changes the size of the view’s frame so that it matches the width and height of the new image. The position of the view’s frame does not change.
Check the size of the frame of your annotation view, which is where your object can receive touches.
I implemented something similar in the following manner
Create a subclass of the Gesture Recognizer class that handles touches
Create a subclass of the UIImage class, this class uses the recognizer class to handle your gestures
Use this in the annotation
The gesture recognizer subclass will handle your gestures if you perform them at any point in the image. This should help you.
Keep us updated on whether this solution worked for u...
#jhabbott solution never worked for me, as I mentioned here.
I have an image and a label side by side. The image was shown by setting annotationview image property, and the label by adding an UILabel
I redirected the func point(inside:with:) method to the UILabel one (which included the image zone) and hitTest did return exactly the same view whether I clicked on the label or the image. But label click did not produce any callback...
Finally, I ended up by enlarging the MKAnnotationView frame to enclose label + image, I set annotationView.image to nil, and I created my custom UIImageView.
Because I wanted the anchor point at the middle of the image, I had to set a custom one :
self.frame = CGRect(x: 0, y: 0, width:
self.myLabel.frame.width, height: self.myLabel.frame.height)
self.centerOffset = CGPoint(x: self.frame.width/2, y: self.myImageView.frame.height/2)
Then I deleted point(inside:with:) and hitTest(point:with:) overrides that did nothing.
And now, for the first time, my annotation view is completely reactive.

Resources