views:

1992

answers:

7

Is there a simple way of allowing interaction with a button in a UIView that lies under another UIView - where there are no actual objects from the top UIView on top of the button?

For instance, at the moment I have a UIView (A) with an object at the top and an object at the bottom of the screen and nothing in the middle. This sits on top of another UIView that has buttons in the middle (B). However, I cannot seem to interact with the buttons in the middle of B.

I can see the buttons in B - I've set the background of A to clearColor - but the buttons in B do not seem to receive touches despite the fact that there are no objects from A actually on top of those buttons.

EDIT - I still want to be able to interact with the objects in the top UIView

Surely there is a simple way of doing this?

Many thanks,

D

+2  A: 

You have to set upperView.userInteractionEnabled = NO or the upper view will intercept the touches.

The Interface Builder version of this is a checkbox at the bottom of the View Attributes panel called "User Interaction Enabled". Uncheck it and you should be good to go.

Benjamin Cox
Sorry - should have said. I still want to be able to interact with the objects in the top UIView.
delany
A: 

I have never built a complete user interface using the UI toolkit, so I don't have much experience with it. Here is what I think should work though.

Every UIView, and this the UIWindow, has a property subviews, which is an NSArray containing all the subviews.

The first subview you add to a view will receive index 0, and the next index 1 and so forth. You can also replace addSubview: with insertSubview: atIndex: or insertSubview:aboveSubview: and such methods that can determine the position of your subview in the hierarchy.

So check your code to see which view you add first to your UIWindow. That will be 0, the other will be 1.
Now, from one of your subviews, to reach another you would do the following:

UIView * theOtherView = [[[self superview] subviews] objectAtIndex: 0];
// or using the properties syntax
UIView * theOtherView = [self.superview.subviews objectAtIndex:0];

Let me know if that works for your case!


(below this marker is my previous answer):

If views need to communicate with each other, they should do so via a controller (that is, using the popular MVC model).

When you create a new view, you can make sure it registers itself with a controller.

So the technique is to make sure your views register with a controller (which can store them by name or whatever you prefer in a Dictionary or Array). Either you can have the controller send a message for you, or you can get a reference to the view and communicate with it directly.

If your view doesn't have a link back the controller (which may be the case) then you can make use of singletons and/or class methods to get a reference to your controller.

nash
Thanks for your reply - but I'm not sure I understand. Both views have a controller - the issue is that, with one view on top of the other, the bottom view is not picking up events (and forwarding them to its controller), even through there is no object actually 'blocking' those events in the top view.
delany
Do you have a UIWindow at the top of our UIViews? If you do, the events should be propagated and you shouldn't need to do any "magic".Have a read about [Window and Views][1] at Apple Dev center (and by all means, add another comment if this doesn't help you!) [1]: http://developer.apple.com/iphone/library/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/WindowsandViews/WindowsandViews.html
nash
Yes, absolutely - a UIWindow at the top of the hierarchy.
delany
I've updated my answer to include the code for going through a hierarchy of views. Let me know if you need any other help!
nash
Thanks for your help nash - but I'm fairly experienced with building these interfaces and my current one is set up correctly as far as I know. The issue seems to be the default behavior of full views when they are placed on top of others.
delany
A: 

There's something you can do to intercept the touch in both views.

Top view:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
   // Do code in the top view
   [bottomView touchesBegan:touches withEvent:event]; // And pass them on to bottomView
   // You have to implement the code for touchesBegan, touchesEnded, touchesCancelled in top/bottom view.
}

But that's the idea.

Alexandre Cassagne
This is certainly possible - but is a lot of work (you would have to roll your own touch-sensitive objects in the bottom layer (e.g. buttons), I think?) and it seems odd that one would have to roll your own in this way to get behavior that would seem to be intuitive.
delany
I'm not sure maybe we should just give it a try.
Alexandre Cassagne
+5  A: 

You should create a UIView subclass for your top view and override the following method:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // UIView will be "transparent" for touch events if we return NO
    return (point.y < MIDDLE_Y1 || point.y > MIDDLE_Y2);
}

You may also look at the hitTest:event: method.

gyim
Thanks gyim - I'll give it a go. That seems to be the sort of thing I'm after!
delany
thankssss!!!!! you saved my life! I was banging my head on the wall for hours trying to figure out how to make invisible to touch some parts of a view! Wow! You are da best!!!
Digital Robot
+2  A: 

There are several ways you could handle this. My favorite is to override hitTest:withEvent: in a view that is a common superview (maybe indirectly) to the conflicting views (sounds like you call these A and B). For example, something like this (here A and B are UIView pointers, where B is the "hidden" one, that is normally ignored):

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
  CGPoint pointInB = [B convertPoint:point fromView:self];
  if ([B pointInside:pointInB withEvent:event]) return B;
  return [super hitTest:point withEvent:event];
}

You could also modify the pointInside:withEvent: method as gyim suggested. This lets you achieve essentially the same result by effectively "poking a hole" in A, at least for touches.

Another approach is event forwarding, which means overriding touchesBegan:withEvent: and similar methods (like touchesMoved:withEvent: etc) to send some touches to a different object than where they first go. For example, in A, you could write something like this:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  if ([self shouldForwardTouches:touches]) {
    [B touchesBegan:touches withEvent:event];
  } else {
    // Do whatever A does with touches.
  }
}

However, this will not always work the way you expect! The main thing is that built-in controls like UIButton will always ignore forwarded touches. Because of this, the first approach is more reliable.

There's a good blog post explaining all this in more detail, along with a small working xcode project to demo the ideas, available here:

http://bynomial.com/blog/?p=74

Tyler
A: 

My solution here:

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint pointInView = [self.toolkitController.toolbar convertPoint:point fromView:self];

    if ([self.toolkitController.toolbar pointInside:pointInView withEvent:event]) {
       self.userInteractionEnabled = YES;
    } else {
       self.userInteractionEnabled = NO;
    }

    return [super hitTest:point withEvent:event];
}

Hope this helps

Jerry
+1  A: 

Custom implementation of pointInside:withEvent: indeed seemed like the way to go, but dealing with hard-coded coordinates seemed odd to me. So I ended up checking whether the CGPoint was inside the button CGRect using the CGRectContainsPoint() function:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {

    if(CGRectContainsPoint(disclosureButton.frame, point))
        return YES; // touched button!

    return NO;
}
Sam V