Detecting hits in UIScrollView while still letting it do it's thing - iphone

Is there a simple way to detect if someone touched a UIScrollView without having to disable user interactions?.
I know this has been answered a few times before, but every answer I find is somebody wanting to detect hits in an image. I don't have an image. What I do have is a scroll view with a number of text fields embedded in it. They used to be inside a UIControl, from which I could detect a touchDown and call resignFirstResponder on all my textfields (for when the keyboard is up). But when they are in a UIScrollView, I can't seem to find a simple way to do this.
I don't really want to have to write code to do the scrolling myself, which is what I assume I have to do if I disable user interactions and grab the touchesBegan message. But I may be wrong. I'm still a little new at this, but this is the first time I haven't been able to figure out the answer by reading the code doc and googling ...
Thanks for any help,
J

Override touchesBegan, do your processing, and call the super implementation so that the scroll view still gets the message.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//call resignFirstResponder
[super touchesBegan:touches withEvent:event];
}

Ok, now I feel a little silly answering this myself. I'm sure the other suggested answer would have worked also, but I found something else while browsing in other topics which totally fixed this for me.
I added this to my viewDidLoad:
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(backgroundTap:)];
[scrollView addGestureRecognizer: singleTap];
Where backgroundTap is
- (IBAction)backgroundTap:(id)sender
is the function I was originally calling from touchDown in UIControl to resign all keyboard firstResponders.
This works perfectly and adds very little code. And I DON'T have to disable user interactions! Yay!
Thanks all for your comments and help.
J

I fiddled with this for a while, and found that simple touchesBegan and touchesEnd type stuff didn't really work.
They don't work because because of the gestures. For example touchesEneded never fires when you swipe to scroll (at least not in the UIScrollView). So while you can detect when a touch event starts, you'll never know when it ends.
I wasn't real keen on including a gesture recognizer for simple stuff and discovered that you can find out about the state of the scrolling better from interacting with the UIScrollView directly.
And it's not that painful. Just add the protocol, then add the functions you need for detection.
In the view (class) that contains the UIScrolView, add the protocol, then added any the functions from here to your view (class).
Example:
// --------------------------------
// In the "h" file:
// --------------------------------
#interface myViewClass : UIViewController <UIScrollViewDelegate> // <-- Adding the protocol here
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView;
// --------------------------------
// In the "m" file:
// --------------------------------
#implementation BlockerViewController
UIScrollView *scrollView;
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
NSLog(#"end decel");
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
NSLog(#"end dragging");
}
// All of the available functions are here:
// https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIScrollViewDelegate_Protocol/Reference/UIScrollViewDelegate.html

Related

iphone - the order of the touch (tap) event

I have a UIView and a UIWebView.
The UIWebView is a child view of the UIView.
The UIWebView contains a youtube video and is set as to let the video fit to the UIWebView.
I have a UITapGesture associated with the parent UIView for single tap, say, if a user single tap the whole view, it will invoke A.
So, when the UIWebView loads the youtube video, there is a button on top of the video waiting for users to click to play. But now if I click the button, the youtube is played, but also the A is invoked too. This is what I don't want.
How should I solve it?
I thought the touch/tap event should be in a order and if the button is clicked, it should absorb the event and not give the UIView any more.
I also tried to add another UIView under the UIWebView, and attach that gesture to the underlying view. However, it still doesn't work.
How can I do to let the button over the youtube video independent?
thanks
Try add this code in your view's .m file (if you are using UIView, subclass it):
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
CGPoint p = [self convertPoint:point toView:webView];
if ([webView pointInside:p withEvent:event]) {
return [webView hitTest:p withEvent:event];
}
return [super hitTest:point withEvent:event];
}
Since I did not try this code, tell me if it doesn't work. There are still many ways to deal with the situation. :)

Passing touch events to iOS status bar

I have a UIWindow with windowLevel set to UIWindowLevelStatusBar+1. This window has a single semi-transparent view that blocks the status bar. I need to sometimes pass touch events from the view on to the status bar. Any ideas?
So, it seems to be doable with a custom subclass of UIWindow overriding hitTest:withEvent: that manually detects a touch in the subview, and always returns nil.
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([event type]==UIEventTypeTouches) {
UIView *v=[super hitTest:point withEvent:event];
if (customSubViewthatCoversStatusBarOnly==v)
//doLotsOfCoolStuff
}
return nil;
}
Status bar recognizes all touches, so there is no breakage with scroll-to-top, return to call, VoiceOver, etc.. And I still get to intercept taps on statusbar.
I hacked this up just now. I will probably upload an update to App Store later this week with a more mature version of this, will see how much complaining Apple will do.
EDIT - 7th April:
Was approved by Apple. Works flawlessly.
You might find this component over on github helpful.
Otherwise, Cocoa with Love blog post is really useful to read perhaps.
As far as I understand this, you should use - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event to implement that behavior. Basically, you either return self if you want to handle the touch event or [super hitTest:point withEvent:event] to let the status bar handle the touch event.
Check out the UIView Class Reference for more.
EDIT: As Jonathan mentioned, Apple might not approve this.

How to detect touch on UIImageView inside UIScrollview?

I have a UIScrollview in my app and I populate it with LOTS of UIImageViews approx 900. They are all very small and consist of only two different images over and over again.
Now I am having a lot of trouble detecting a touch on one of these UIImageViews.
I have assigned them all a unique TAG so as to be able to distinguish between them but I am really struggling to detect the touch.
The goal is just to be able to change the image of the touched UIImageView.
Due to the large amount of views involved a simple loop checking touch coordinates against each UIImageViews frame is just hanging my app.
Does anyone have any suggestions?
Thanks in Advance.
Ben
In my case the easiest way to do it was adding a gesture recognizer:
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(singleTapGestureCaptured:)];
//Default value for cancelsTouchesInView is YES, which will prevent buttons to be clicked
singleTap.cancelsTouchesInView = NO;
[myScrollView addGestureRecognizer:singleTap];
Then use this method to capture the touch:
- (void)singleTapGestureCaptured:(UITapGestureRecognizer *)gesture
{
CGPoint touchPoint=[gesture locationInView:myScrollView];
}
UIImageView has touch processing turned off by default. To change that set userInteractionEnabled to YES (see the docs here http://developer.apple.com/library/ios/#DOCUMENTATION/UIKit/Reference/UIImageView_Class/Reference/Reference.html)
Also if you want to know which view was hit in a view hierarchy you can use hitTest:withEvent: (see docs in UIView) which will be much faster than you looping through the hierarchy.
On a wider note I don't think having 900 UIImageView's on the screen all at once is going to be a good long term strategy. Have you investigated drawing the screen's content via CoreGraphics?
The best way to solve this issue is to subclass UIScrollView, add touchesBegan etc methods to it.
#interface MyScroll : UIScrollView
{
}
You could easily implement this method on the UIImageView object:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([self pointInside:point withEvent:event]) {
//You clicked inside the object
}
return [super hitTest:point withEvent:event]
}
Use the return to tell the application the next touch responder (such as the uiimageview or the scrollview).

Is it possible to differentiate between a long press and a tap on a button?

Can we call different actions / delegates in response to the two different events of
A tap on a UIButton
A tap-and-hold on a UIButton
?
Yes, it's reasonably easy to implement this using a UILongPressGestureRecognizer (on iPhone OS 3.2+). A long press will be handled by the gesture recognizer, and a short tap will pass through to the button's normal action.
For example, I subclassed UIButton and added the following method for specifying a long touch action to go along with a tap (longPressGestureRecognizer is an instance variable):
- (void)setLongTouchAction:(SEL)newValue
{
if (newValue == NULL)
{
[self removeGestureRecognizer:longPressGestureRecognizer];
[longPressGestureRecognizer release];
longPressGestureRecognizer = nil;
}
else
{
[longPressGestureRecognizer release];
longPressGestureRecognizer = nil;
longPressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:[[self allTargets] anyObject] action:newValue];
[self addGestureRecognizer:longPressGestureRecognizer];
}
}
I then could do the following to set both short tap and long press actions that will be handled by the same target:
[undoButton addTarget:self action:#selector(performUndo:) forControlEvents:UIControlEventTouchUpInside];
[undoButton setLongTouchAction:#selector(showUndoOptions:)];
As you can see, this is useful for the undo buttons you see in title bars of many iPad applications.
Brad Larson's answer looks pretty good but here's another one that might give you a bit more flexibility/control of what you want or might want to do.
You subclass UIButton, you override the touchesBegan and touchesEnded methods so that when the user starts a touch you call
[self performSelector:#selector(detecetedLongTap) withObject:nil afterDelay:1.0];
and in the touchesEnded you call:
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(detecetedLongTap) object:nil];
to cancel the event if the finger was lifted too soon.
You can get full code for this in this blog post:
http://www.isignmeout.com/adding-long-tap-functionality-uibutton/
The best solution I can think of, is to create another class, and subclass UIButton. Then on Interface Builder (if that's what you're using), you can set the button's class to the custom class you just created.
So in this new class, you have to override a method called
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
This is basically telling you that someone pressed down on your button. The touches is an NSSet and it holds all the information for all the fingers that are pressing down on the screen. If you only are interested in the one that's pressing on the button itself, you'll probably have something like:
NSSet *myTouches = [event touchesForView:self.view];
So now that you have the touches that correspond to your button, you have to find out how many times the user tapped on that button. You do that with something like:
NSUInteger numTaps = [[myTouches anyObject] tapCount];
If this number is 2, that means the user just double tapped your button. Now comes the next part. How do you know if the user is holding that button? Well when the user lets go of the screen, another method gets called. You'll need to override that one as well:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
This is where you know if the person has stopped touching the screen or if his finger is still on it. If his finger is still on it, then this event hasn't been called yet.
Now enough with the background
Here's my suggestion to you. I suggest you override the touchesBegan: method and check if the number of taps in the button is 2. If so, then start a timer that does what you need it to do, for as long as you need it to be done, and then on the touchesEnded: method, you'll go ahead and stop that timer, and this way you will have done whatever it is that you needed to do, for as long as you needed to do it OR as long as the user has held on to the button.
I hope this helps, obviously I didn't write the whole code for you, you'll have to experiment and research that stuff, but if you have any questions, I'll be happy to lend a helping hand :)

ScrollOffset in UIWebView?

I'm having a really hard time understanding delegates and object inheritance (if I may use this word) and I think I need a simple (or so I think) thing: catch scrollViewDidScroll event in UIWebView and get offset (basically, just to know if scroll is not on top/bottom, so I could hide navigation and tab bars).
Is there any way I could do it? I already using UIWebviewDelegate in my view controller to "shouldStartLoadWithRequest". Maybe I could some how use UIScrollViewDelegate too for scrollViewDidScroll? If yes, then how?
I really have trouble understanding delegates. I've red some articles, but still, in practice, I can't manage to use them.
Any help or info would be lovely.
Thank you in advance!
To retrieve scroll events on UIWebView I personnaly use this code to get the scrollview that is inside the UIWebView :
- (void) addScrollViewListener
{
UIScrollView* currentScrollView;
for (UIView* subView in self.myWebView.subviews) {
if ([subView isKindOfClass:[UIScrollView class]]) {
currentScrollView = (UIScrollView*)subView;
currentScrollView.delegate = self;
}
}
}
It's working. You can also use it to call [currentScrollView setContentOffset:offSet animated:YES]; The only problem may be not to pass Apple code checking. I don't know yet since I'm still in coding phase.
[UPDATE] The app with this code is in the app store for 4 months now and used by 40 000 users. I didn't have any trouble [UPDATE]
You can use the following methods to solve your problem.
For getting the pageOffset:
int pageYOffset = [[webViewObj stringByEvaluatingJavaScriptFromString:#"window.pageYOffset"] intValue];
For getting the total scroll height of a webpage:
int scrollHeight = [[webViewObj stringByEvaluatingJavaScriptFromString:#"document.documentElement.scrollHeight"] intValue];
For scrolling the webpage to a particular offset:
[webViewObj stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:#"document.body.scrollTop = %d",scrollHeight ]];
I made a modification to detect the class with isKindOfClass. Works - but may have issues with Apple code checking as stated above.
UIScrollView* currentScrollView;
for (UIView* subView in terms.subviews) {
if ([subView isKindOfClass:[UIScrollView class]]) {
NSLog(#"found scroll view");
currentScrollView = (UIScrollView *)subView;
currentScrollView.delegate = self;
}
}
Old thread, I know -
As of iOS 5.0 you can use
myAccountWebView.scrollview
to access content size and offset.
There is a scrolling view in the UIWebView, but it a) isn't a UIScrollView, and b) is something Apple considers a private implementation detail (and you should too). I only really have two suggestions:
File a bug with Apple asking them to expose more of the infrastructure of the web view, or at least add some more delegate methods by which we can be notified of these sorts of events.
Add some JavaScript code to your page that listens from scroll events, and notifies your app of them.
The basic foundation of #2 is to load a fake URL, and have your web view delegate process (and abort!) that load. (This question has come up a few times here on Stack Overflow.)
UPDATE:
As of iOS 5, there is now a public scrollView property on UIWebView that you can use to customize scrolling behavior. The exact view hierarchy of the web view remains an undocumented implementation detail, but this gives you a sanctioned way to access this piece of it.
It's a good question. UIWebView is not a subclass of UIScrollView, although I can see why one might think it is. That means using the UIScrollViewDelegate methods is not an option to do what you want, and the UIWebViewDelegate protocol does not respond to those scrolling event type of messages. I don't think there's an easy way to detect scrolling events in a web view.
I tired the delegate method and found it prevented the view from scrolling when the keyboard was shown. I found that by adding an observer you do not override the current delegate and will prevent you from effecting the webview performance.
for (UIView* subView in myAccountWebView.subviews) {
if ([subView isKindOfClass:[UIScrollView class]])
{
NSLog(#"found scroll view");
[((UIScrollView*)subView) addObserver:self forKeyPath:#"contentOffset" options:NSKeyValueObservingOptionNew context:nil];
}
}