Can I detect if higher subview has been touched? - iphone

I've got a big UIView that responds to touches, and it's covered with lots of little UIViews that respond differently to touches. Is it possible to touch anywhere on screen and slide around, and have each view know if it's being touched?
For example, I put my finger down on the upper left and slide toward the lower right. The touchesBegan/Moved is collected by the baseView. As I pass over itemView1, itemView2, and itemView3, control passes to them. If I lift my finger while over itemView2, it performs itemView2's touchesEnded method. If I lift my finger over none of the items, it performs baseView's touchesEnded.
At the moment, if I touch down on baseView, touchEnded is always baseView and the higher itemViews are ignored.
Any ideas?

If I understand correctly, the touchesEnded event is detected, but not by the subview that needs to know about it. I think this might work for you:
In a common file, define TOUCHES_ENDED_IN_SUPERVIEW as #"touches ended in superview".
In the touchesEnded method of the containing view that is firing, add
[[NSNotificationCenter defaultCenter] postNotificationName: TOUCHES_ENDED_IN_SUPERVIEW object: self];
In the touchesBegan of the subviews, add
[[NSNotificationCenter defaultCenter] addObserver: self
selector: #selector(touchesEnded:)
name: TOUCHES_ENDED_IN_SUPERVIEW
object: self.superview];
In the touchesEnded methods of the subviews, use your normal logic for the event, and also add
[[NSNotificationCenter defaultCenter] removeObserver: self name: TOUCHES_ENDED_IN_SUPERVIEW object: self.superview];
Remember to put [[NSNotificationCenter defaultCenter] removeObserver: self] in your dealloc as well, in case it is possible to leave the page without getting the touchesEnded event.
You might want the notification to send its message to a special touchesEndedInSuperview method, which would invoke touchesEnded itself, but that depends on whether you have any special processing to do in that case.

You can use something like this:
-(void)touchesEnded: (NSSet *)touches
withEvent: (UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView: touch.view];
if (CGRectContainsPoint(control1.frame, location)) {
[self control1Action];
} else if (CGRectContainsPoint(control2.frame, location)) {
[self control2Action];
}
}

Related

How to Implement Touch Up Inside in touchesBegan, touchesEnded

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.

How to pass touch event to another object?

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.

How to detect touches in status bar

I have custom view in my application which can be scrolled by the user. This view, however, does not inherit from UIScrollView. Now I want the user to be able to scroll this view to the top, just as any other scrollable view allows. I figured that there is no direct way to do so.
Google turned up one solution: http://cocoawithlove.com/2009/05/intercepting-status-bar-touches-on.html This no longer works on iOS 4.x. That's a no-go.
I had the idea of creating a scrollview and keeping it around somewhere, just to catch it's notifications and then forward them to my control. This is not a nice way to solve my problem, so I am looking for "cleaner" solutions. I like the general approach of the aforementioned link to subclass UIApplication. But what API can give me reliable info?
Are there any thoughts, help, etc...?
Edit: Another thing I don't like about my current solution is that it only works as long as the current view does not have any scroll views. The scroll-to-top gesture works only if exactly one scroll view is around. As soon as the dummy is added (see my answer below for details) to a view with another scrollview, the gesture is completely disabled. Another reason to look for a better solution...
Finally, i've assembled the working solution from answers here. Thank you guys.
Declare notification name somewhere (e.g. AppDelegate.h):
static NSString * const kStatusBarTappedNotification = #"statusBarTappedNotification";
Add following lines to your AppDelegate.m:
#pragma mark - Status bar touch tracking
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
CGPoint location = [[[event allTouches] anyObject] locationInView:[self window]];
CGRect statusBarFrame = [UIApplication sharedApplication].statusBarFrame;
if (CGRectContainsPoint(statusBarFrame, location)) {
[self statusBarTouchedAction];
}
}
- (void)statusBarTouchedAction {
[[NSNotificationCenter defaultCenter] postNotificationName:kStatusBarTappedNotification
object:nil];
}
Observe notification in the needed controller (e.g. in viewWillAppear):
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(statusBarTappedAction:)
name:kStatusBarTappedNotification
object:nil];
Remove observer properly (e.g. in viewDidDisappear):
[[NSNotificationCenter defaultCenter] removeObserver:self name:kStatusBarTappedNotification object:nil];
Implement notification-handling callback:
- (void)statusBarTappedAction:(NSNotification*)notification {
NSLog(#"StatusBar tapped");
//handle StatusBar tap here.
}
Hope it will help.
Swift 3 update
Tested and works on iOS 9+.
Declare notification name somewhere:
let statusBarTappedNotification = Notification(name: Notification.Name(rawValue: "statusBarTappedNotification"))
Track status bar touches and post notification. Add following lines to your AppDelegate.swift:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
let statusBarRect = UIApplication.shared.statusBarFrame
guard let touchPoint = event?.allTouches?.first?.location(in: self.window) else { return }
if statusBarRect.contains(touchPoint) {
NotificationCenter.default.post(statusBarTappedNotification)
}
}
Observe notification where necessary:
NotificationCenter.default.addObserver(forName: statusBarTappedNotification.name, object: .none, queue: .none) { _ in
print("status bar tapped")
}
So this is my current solution, which works amazingly well. But please come with other ideas, as I don't really like it...
Add a scrollview somewhere in your view. Maybe hide it or place it below some other view etc.
Set its contentSize to be larger than the bounds
Set a non-zero contentOffset
In your controller implement a delegate of the scrollview like shown below.
By always returning NO, the scroll view never scrolls up and one gets a notification whenever the user hits the status bar. The problem is, however, that this does not work with a "real" content scroll view around. (see question)
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
{
// Do your action here
return NO;
}
Adding this to your AppDelegate.swift will do what you want:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesBegan(touches, withEvent: event)
let events = event!.allTouches()
let touch = events!.first
let location = touch!.locationInView(self.window)
let statusBarFrame = UIApplication.sharedApplication().statusBarFrame
if CGRectContainsPoint(statusBarFrame, location) {
NSNotificationCenter.defaultCenter().postNotificationName("statusBarSelected", object: nil)
}
}
Now you can subscribe to the event where ever you need:
NSNotificationCenter.defaultCenter().addObserverForName("statusBarSelected", object: nil, queue: nil) { event in
// scroll to top of a table view
self.tableView!.setContentOffset(CGPointZero, animated: true)
}
Thanks Max, your solution worked for me after spending ages looking.
For information :
dummyScrollView = [[UIScrollView alloc] init];
dummyScrollView.delegate = self;
[view addSubview:dummyScrollView];
[view sendSubviewToBack:dummyScrollView];
then
dummyScrollView.contentSize = CGSizeMake(view.frame.size.width, view.frame.size.height+200);
// scroll it a bit, otherwise scrollViewShouldScrollToTop not called
dummyScrollView.contentOffset = CGPointMake(0, 1);
//delegate :
- (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView
{
// DETECTED! - do what you need to
NSLog(#"scrollViewShouldScrollToTop");
return NO;
}
Note that I had a UIWebView also which I had to hack a bit with a solution I found somewhere :
- (void)webViewDidFinishLoad:(UIWebView *)wv
{
[super webViewDidFinishLoad:wv];
UIScrollView *scroller = (UIScrollView *)[[webView subviews] objectAtIndex:0];
if ([scroller respondsToSelector:#selector(setScrollEnabled:)])
scroller.scrollEnabled = NO;
}
Found a much better solution which is iOS7 compatible here :http://ruiaureliano.tumblr.com/post/37260346960/uitableview-tap-status-bar-to-scroll-up
Add this method to your AppDelegate:
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
CGPoint location = [[[event allTouches] anyObject] locationInView:[self window]];
if (CGRectContainsPoint([UIApplication sharedApplication].statusBarFrame, location)) {
NSLog(#"STATUS BAR TAPPED!");
}
}
I implemented this by adding a clear UIView over the status bar and then intercepting the touch events
First in your Application delegate application:didFinishLaunchingWithOptions: add these 2 lines of code:
self.window.backgroundColor = [UIColor clearColor];
self.window.windowLevel = UIWindowLevelStatusBar+1.f;
Then in the view controller you wish to intercept status bar taps (or in the application delegate) add the following code
UIView* statusBarInterceptView = [[[UIView alloc] initWithFrame:[UIApplication sharedApplication].statusBarFrame] autorelease];
statusBarInterceptView.backgroundColor = [UIColor clearColor];
UITapGestureRecognizer* tapRecognizer = [[[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(statusBarClicked)] autorelease];
[statusBarInterceptView addGestureRecognizer:tapRecognizer];
[[[UIApplication sharedApplication].delegate window] addSubview:statusBarInterceptView];
In the statusBarClicked selector, do what you need to do, in my case I posted a notification to the notification center so that other view controllers can respond to the status bar tap.
Use an invisible UIScrollView. Tested at iOS 10 & Swift 3.
override func viewDidLoad() {
let scrollView = UIScrollView()
scrollView.bounds = view.bounds
scrollView.contentOffset.y = 1
scrollView.contentSize.height = view.bounds.height + 1
scrollView.delegate = self
view.addSubview(scrollView)
}
func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
debugPrint("status bar tapped")
return false
}
You can track status bar tap by using following code:
[[NSNotificationCenter defaultCenter] addObserverForName:#"_UIApplicationSystemGestureStateChangedNotification"
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
NSLog(#"Status bar pressed!");
}];
One way, might not be the best, could be to put a clear UIView on top of the status bar and intercept the touches of the UIView, might help you out if nothing else comes up...
If you're just trying to have a UIScrollView scroll to the top when the status bar is tapped, it's worth noting that this is the default behavior IF your view controller has exactly one UIScrollView in its subviews that has scrollsToTop set to YES.
So I just had to go and find any other UIScrollView (or subclasses: UITableView, UICollectionView, and set scrollsToTop to be NO.
To be fair, I found this info in the post that was linked to in the original question, but it's also dismissed as no longer working so I skipped it and only found the relevant piece on a subsequent search.
For iOS 13 this has worked for me, Objective-C category of UIStatusBarManager
#implementation UIStatusBarManager (CAPHandleTapAction)
-(void)handleTapAction:(id)arg1 {
// Your code here
}
#end

iphone keyboard touch events

I need to be able to detect touch events on the keyboard. I have an app which shows a screen which occurs after a certain period of inactivity (i.e. no touch events) To solve this issue, I have subclassed my UIWindow and implemented the sendEvent function, which allows me to get touch events on the whole application by implementing the method in one place. This works everywhere beside when the keyboard is presented and the user is typing on the keyboard. What I need to know is that is there a way to detect touch events on the keyboard, kind of like what sentEvent does for uiWindow. Thanks in advance.
found a solution to the problem. if you observe the following notifications, you are able to get an event when the key is pressed. I added these notifications in my custom uiwindow class so doing it at one place will allow me to get these touch events throughout the application.
[[NSNotificationCenter defaultCenter] addObserver: self selector: #selector(keyPressed:) name: UITextFieldTextDidChangeNotification object: nil];
[[NSNotificationCenter defaultCenter] addObserver: self selector: #selector(keyPressed:) name: UITextViewTextDidChangeNotification object: nil];
- (void)keyPressed:(NSNotification*)notification
{ [self resetIdleTimer]; }
anyways, hope it helps someone else.
iPhoneDev: here is what I am doing.
I have a custom UIWindow object. In this object, there is a NSTimer that is reset whenever there is a touch. To get this touch you have to override the sendEvent method of UIWindow.
this is what the sendEvent method looks like in my custom window class:
- (void)sendEvent:(UIEvent *)event
{
if([super respondsToSelector: #selector(sendEvent:)])
{
[super sendEvent:event];
}
else
{
NSLog(#"%#", #"CUSTOM_Window super does NOT respond to selector sendEvent:!");
ASSERT(false);
}
// Only want to reset the timer on a Began touch or an Ended touch, to reduce the number of timer resets.
NSSet *allTouches = [event allTouches];
if ([allTouches count] > 0)
{
// anyObject works here.
UITouchPhase phase = ((UITouch *)[allTouches anyObject]).phase;
if (phase == UITouchPhaseBegan || phase == UITouchPhaseEnded)
{
[self resetIdleTimer];
}
}
}
here is the resetIdleTimer:
- (void)resetIdleTimer
{
if (self.idleTimer)
{
[self.idleTimer invalidate];
}
self.idleTimer = [NSTimer scheduledTimerWithTimeInterval:PASSWORD_TIMEOUT_INTERVAL target:self selector:#selector(idleTimerExceeded) userInfo:nil repeats:NO];
}
after this, in the idleTimerExceeded, I send a message to the window delegates, (in this case, the appDelegate).
- (void)idleTimerExceeded
{
[MY_CUSTOM_WINDOW_Delegate idleTimeLimitExceeded];
}
When I create this custom window object in the appDelegate, I set the appDelegate as the delegate for this window. And in the appDelegate definition of idleTimeLimitExceeded is where I do what I have to when the timer expires. They key thing is the create the custom window and override the sendEvent function. Combine this with the two keyboard notification shown above I added in the init method of the custom window class and you should be able to get 99% of all touch events on screen anywhere in the application.

How does overlayViewTouched notification work in the MoviePlayer sample code

I have a question regarding the MoviePlayer sample code provided by apple.
I don't understand how the overlayViewTouch notification works. The NSlog message I added to it does not get sent when I touch the view (not button).
// post the "overlayViewTouch" notification and will send
// the overlayViewTouches: message
- (void)overlayViewTouches:(NSNotification *)notification
{
NSLog(#"overlay view touched");
// Handle touches to the overlay view (MyOverlayView) here...
}
I can, however, get the NSlog notification if I place it in -(void)touchesBegan in "MyOverlayView.m". Which makes me think it is recognizing touches but not sending a notification.
// Handle any touches to the overlay view
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch* touch = [touches anyObject];
if (touch.phase == UITouchPhaseBegan)
{
NSLog(#"overlay touched(from touchesBegan")
// IMPORTANT:
// Touches to the overlay view are being handled using
// two different techniques as described here:
//
// 1. Touches to the overlay view (not in the button)
//
// On touches to the view we will post a notification
// "overlayViewTouch". MyMovieViewController is registered
// as an observer for this notification, and the
// overlayViewTouches: method in MyMovieViewController
// will be called.
//
// 2. Touches to the button
//
// Touches to the button in this same view will
// trigger the MyMovieViewController overlayViewButtonPress:
// action method instead.
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:OverlayViewTouchNotification object:nil];
}
}
Can anyone shed light on what I am missing or doing wrong?
Thank you.
As it seems to me the sample code is missing the addObserver selector call to the Notification. An example of the registration can be found in the AppDelegate:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(moviePreloadDidFinish:)
name:MPMoviePlayerContentPreloadDidFinishNotification
object:nil];
As in NSNotificationCenter Documentation
When an object (known as the notification sender) posts a notification, it sends an NSNotification object to the notification center. The notification center then notifies any observers for which the notification meets the criteria specified on registration by sending them the specified notification message, passing the notification as the sole argument.
If there are no observers no one will be informed by NSNotificationCenter.
Just add the appropriate register in init for example.
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(overlayViewTouches:)
name:OverlayViewTouchNotification
object:nil];
It's because the overlay view is small. You can see the area covered by the overlay view by changing the background color of the overlay view. The notification will be delivered when you touch the area.