I'm looking to shorten the touch delay on a UIScrollView, but I don't want to use setDelaysContentTouches:NO; I still want there to be a slight delay but my users are complaining about it being too long.
Is there a way to do this?
The doc says
If the user then drags their finger far enough before the timer
elapses, the scroll view cancels any tracking in the subview and
performs the scrolling itself. Subclasses can override the
touchesShouldBegin:withEvent:inContentView:, pagingEnabled, and
touchesShouldCancelInContentView: methods (which are called by the
scroll view) to affect how the scroll view handles scrolling gestures.
So i think there is no easy way to do it. You probably have to reimplement the whole timer system in those methods.
I just came across this problem and this is my solution:
Subclass UIScrolView
Add override these methods:
- (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view
{
self.lastTimestamp = [NSDate date];
return [super touchesShouldBegin:touches withEvent:event inContentView:view];
}
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
NSDate *now = [NSDate date];
if (-[self.lastTimestamp timeIntervalSinceDate:now] < _delay)
return YES;
return NO;
}
Related
How can we get the time a user spent to finish a swipe right or swipe left action? Already googled but no luck. If there is not built-in solution already, will touch begin and touch end together work?
Measure the start and end time, and calculate the difference in the UIGestureRecognizer callback method for gestureRecognizer.state == UIGestureRecognizerStateBegan and gestureRecognizer.state == UIGestureRecognizerStateEnded
You can do this by getting the date (which you create as ivar or property) in the touchesBegan method like this:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
touchStartDate = [NSDate date];
}
Then you fetch the difference in the touches end method:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
NSTimeInterval *touchDuration = [[NSDate date] timeIntervalSinceDate:touchStartDate];
NSLog(#"Touchduration %f",touchDuration);
}
If you only want to get the duration of a swipe left/right gesture you also need to check if the x value of the touch changed. This should be easy.
I'm wondering if someone knows how to implement the "touch up inside" response when a user pushes down then lifts their finger in the touchesBegan, touchesEnded methods. I know this can be done with UITapGestureRecognizer, but actually I'm trying to make it so that it only works on a quick tap (with UITapGestureRecognizer, if you hold your finger there for a long time, then lift, it still executes). Anyone know how to implement this?
Using the UILongPressGesturizer is actually a much better solution to mimic all of the functionality of a UIButton (touchUpInside, touchUpOutside, touchDown, etc.):
- (void) longPress:(UILongPressGestureRecognizer *)longPressGestureRecognizer
{
if (longPressGestureRecognizer.state == UIGestureRecognizerStateBegan || longPressGestureRecognizer.state == UIGestureRecognizerStateChanged)
{
CGPoint touchedPoint = [longPressGestureRecognizer locationInView: self];
if (CGRectContainsPoint(self.bounds, touchedPoint))
{
[self addHighlights];
}
else
{
[self removeHighlights];
}
}
else if (longPressGestureRecognizer.state == UIGestureRecognizerStateEnded)
{
if (self.highlightView.superview)
{
[self removeHighlights];
}
CGPoint touchedPoint = [longPressGestureRecognizer locationInView: self];
if (CGRectContainsPoint(self.bounds, touchedPoint))
{
if ([self.delegate respondsToSelector:#selector(buttonViewDidTouchUpInside:)])
{
[self.delegate buttonViewDidTouchUpInside:self];
}
}
}
}
I'm not sure when it was added, but the property isTouchInside is a life saver for any UIControl derived object (e.g. UIButton).
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
super.endTracking(touch, with: event)
if isTouchInside {
// Do the thing you want to do
}
}
Here's the Apple official docs
You can implement touchesBegan and touchesEnded by creating a UIView subclass and implementing it there.
However you can also use a UILongPressGestureRecognizer and achieve the same results.
I did this by putting a timer that gets triggered in touchesBegan. If this timer is still running when touchesEnded gets called, then execute whatever code you wanted to. This gives the effect of touchUpInside.
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSTimer *tapTimer = [[NSTimer scheduledTimerWithTimeInterval:.15 invocation:nil repeats:NO] retain];
self.tapTimer = tapTimer;
[tapTimer release];
}
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
if ([self.tapTimer isValid])
{
}
}
You can create some BOOL variable then in -touchesBegan check what view or whatever you need was touched and set this BOOL variable to YES. After that in -touchesEnded check if this variable is YES and your view or whatever you need was touched that will be your -touchUpInside. And of course set BOOL variable to NO after.
You can add a UTapGestureRecognizer and a UILongPressGestureRecognizer and add dependency using [tap requiresGestureRecognizerToFail:longPress]; (tap and long press being the objects of added recognizers).
This way, the tap will not be detected if long press is fired.
I referenced the apps called "Comic Strip" and "Balloon Stickies Free"
When i add a speech balloon and touch s.b or s.b's tail, it works. But when i touch tail's around or between s.b and s.b's tail, it doesn't work. And Photo gets touch and works below the s.b.
So i tried to use hitTest:withEvent.
It works when i touch rectangle or tailRect first time. But when i touch other place in the object, and i touch rectangle or tailRect again, it doesn't work.
So how to modify this code ? i don't know .. help me please
- (id)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitView = [super hitTest:point withEvent:event];
if(CGRectContainsPoint(rectangle, currentPt)==YES || CGRectContainsPoint(tailRect, currentPt)==YES)
return hitView;
else
return nil;
}
Try overriding - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event instead.
Or take a look at Ole Begemann's OBShapedButton. Code can easily be modified
to work with UIView instead of UIButton.
See this post horizontal scrolling. Using this code you can get all touch events in a single UIWindow class. You have to write some code to pass control appropriately.
I've made a custom zero-image, Core Graphics drawn UIButton based on Ray Wenderlich's Tutorial (the only modifications are to CoolButton's drawRect: method, altered drawing code), and it works great most of the time. However, sometimes when I click it for a short period of time, it stays in a depressed state and doesn't return to normal.
From here, the only way to get it back to a normal state is via a long press. Simply clicking means it stays depressed.
Another thing to note is that I've hooked Touch Up Inside up to a chain of a few long methods - I don't think it would take more than 0.1 seconds to complete. I've even used dispatch_async in the #selector that is hooked up to Touch Up Inside, so there shouldn't be a delay in the UI updating, I think.
I've put an NSLog in the drawRect: which fires 3 times per button press usually, and it varies what UIControlState the button is in for each press:
For some short presses, it goes Highlighted, Highlighted, Normal
for longer presses, it's Highlighted, Normal, Normal
However, for very short presses, it only fires twice, Highlighted -> Highlighted.
When it's a long press to get it back to Normal, it goes H, N, N.
This has been puzzling me for a while, and I haven't been able to work out why short presses only fire drawRect: twice, or why touchesEnded: doesn't seem to call drawRect:. Perhaps touchesEnded: isn't firing?
I really hope someone can help.
If you really want to generate the button images at runtime, generate them when the button is loaded. Then add them for different states using
[button setImage:btnImage forState:UIControlStateNormal];
You can always turn a view into an image using the following: http://pastie.org/244916
Really though, I'd recommend just making images beforehand. If you don't want to get photoshop, there's plenty of alternatives. The upcoming pixelmator update looks pretty suave, ands it's ridiculously cheap!
Well, well, that was easy. 0.15 second delay works, 0.1 doesn't.
As other said, you can simply generate an image and use that as background (if the image is static). No need photoshop, you may simply generate your image once and then take a snapshot, then cut the image out (with Anteprima) and save it as a png file :)
However, since you said the button is connected to some long methods, this may be why the button stay pressed: if you are not calling those methods in background, then the button will stay pressed since all the task are ended. Try (for a test) to connect the button with a single simple method (say NSLog something) and check if it stay pressed. If not, I suggest to detach your methods in background.
I too ran into a similar problem with a UIButton appearing to stick one state when I customize the drawRect method. My hunch is that the state of the button is changing more than once before drawRect is called. So I just added a custom property that would only be set by methods called during touchDown and touchUpInside events. I had to dispatch threads to do this because of other operations that were holding up the main thread, causing a delay in the redrawHighlighted method.
It may be a little messy, but here's what worked for me:
// CustomButton.h
#interface CustomButton : UIButton
#property (atomic) BOOL drawHighlighted;
// CustomButton.m
#implementation CustomButton
#synthesize drawHighlighted = _drawHighlighted;
- (id)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame]))
{
_drawHighlighted = NO;
[self addTarget:self action:#selector(redrawHighlighted) forControlEvents:UIControlEventTouchDown];
[self addTarget:self action:#selector(redrawNormal) forControlEvents:UIControlEventTouchUpInside];
[self addTarget:self action:#selector(redrawNormal) forControlEvents:UIControlEventTouchDragExit];
}
return self;
}
- (void)drawRect:(CGRect)frame
{
// draw button here
}
- (void)redrawNormal
{
[NSThread detachNewThreadSelector:#selector(redrawRectForNormal) toTarget:self withObject:nil];
}
- (void)redrawHighlighted
{
[NSThread detachNewThreadSelector:#selector(redrawRectForHighlighted) toTarget:self withObject:nil];
}
- (void)redrawRectForNormal
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
_drawHighlighted = NO;
[self setNeedsDisplay];
[pool release];
}
- (void)redrawRectForHighlighted
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
_drawHighlighted = YES;
[self setNeedsDisplay];
[pool release];
}
Another cheap but effective alternative I've found is to create a drawRect: that doesn't do any highlighting, and simply alter the button subclass's alpha property when the user interacts with the button.
For example:
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
self.alpha = 0.3f;
return [super beginTrackingWithTouch:touch withEvent:event];
}
- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event
{
self.alpha = 1.0f;
[super endTrackingWithTouch:touch withEvent:event];
}
- (void)cancelTrackingWithEvent:(UIEvent *)event
{
self.alpha = 0.3f;
[super cancelTrackingWithEvent:event];
}
- (void)drawRect:(CGRect)rect
{
// Your drawing code here sans highlighting
}
I have a custom annotation view on the map, which has a UIButton in it, but the UIButton is not responsive when pressed. I have two main problems with user interaction on the annotation view:
Buttons and other controls are not responsive.
I want the annotation to block touches according to my implementation of - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event - that is if I return YES then I don't want the touches to get sent through to the MKMapView (potentially selecting other annotations that are BEHIND my annotation view), I want to handle the touch myself in this case.
I have made sure userInteractionEnabled is set to YES and I have investigated how touches are sent to the custom annotation view (my subclass of MKAnnotationView) by overriding touchesBegan etc. - but it appears that the touches are usually cancelled (thought I've managed to get touchesEnded a few times) - so it seems like it will even be difficult to manually implement any user-interaction with the custom annotation view.
Does anyone have any insights into allowing more user interaction with MKAnnotationView objects?
I managed to resolve this with the help of a colleague. The solution is to override - (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event. Our assumption is that MKAnnotationView (which your annotation view must inherit from) overrides this to do the 'wrong' thing (presumably so that annotation selection doesn't get blocked between overlapping annotations). So you have to re-override it to do the right thing and return the appropriate UIView, the system will then send the events to it and the user will be able to interact with it :). This has the beneficial (in this case) side-effect that the interactive annotation blocks the selection of annotations that are behind it.
I found that rather than overriding hitTest:withEvent: I could just override pointInside:withEvent: instead and just get it to return YES. I guess that officially I should be doing a point-rect intersect check to ensure the place I'm tapping is within the control element, but in practise, just putting return YES appears to work perfectly well, still allowing you to dismiss the MKAnnotationView by tapping away from it.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
// for testing purposes
BOOL result = [super pointInside:point withEvent:event];
NSLog(#"pointInside:RESULT = %i", result);
return YES;
}
Adding up to the answer of jhabbott, this is what worked for me. I have a custom annotation view MKCustomAnnotationView that holds a custom annotation CustomPin as annotation. That 'pin' holds a UIButton as accessory button replacement which I wanted to get touch events.
My hitTest method would look like this:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *result = [super hitTest:point withEvent:event];
//NSLog(#"ht: %f:%f %d %#", point.x, point.y, [[event touchesForView:self] count], result);
if ([result isKindOfClass:[MKCustomAnnotationView class]])
{
MKCustomAnnotationView *av = (MKCustomAnnotationView *)result;
CustomPin *p = av.annotation;
UIButton *ab = p.accessoryButton;
if (p.calloutActive && point.x >= ab.frame.origin.x)
return ab;
}
return result;
}
The calloutActive bool is probably not necessary in most cases.
For anyone looking to add a tapGesture to an AnnotationView subview then the answer at the bottom of this:
MKannotationView with UIButton as subview, button don't respond
Worked for me:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (CGRectContainsPoint(_button.frame, point)) {
return _button;
}
return [super hitTest:point withEvent:event];
}