avf
avf

Reputation: 850

Allow only one UIView to be touched

I have three UIViews:

views

B1 and B2 are subviews of A.

B1 and B2 may have one or more subviews.

I want that only one view can be touched at the same time.

For example:

The user touches B1 (or one of its subviews) with the first finger, and then, without releasing it, touches B2 (or one of its subviews) with the second finger. Now, B1 should receive all events in its bounds, but not B2. Same thing the other way around.

Is this possible to accomplish without subclassing any of these views?

If not, what's the easiest/cleanest way to do it?

Edit:

About exclusiveTouch and multipleTouchEnabled:

These properties are not valid for my problem.

multipleTouchEnabled: Only determines whether or not multi-touch-events are delivered in the same view. So disabling multi-touch on view A (which is the default anyways) will not block view B1 and B2 from receiving touch events at the same time.

exclusiveTouch: Doesn't work either. As far as I have understood, exclusiveTouch only works inside the frame of the view. So setting exclusiveTouch on view B1 and B2 has no effect. See these two stackoverflows for reference:

Why is UIView exclusiveTouch property not blocking?

Why doesn't UIView.exclusiveTouch work?

Upvotes: 1

Views: 1547

Answers (3)

avf
avf

Reputation: 850

I believe this is not possible without subclassing. If anyone can prove me wrong, I'd be happy to see their solution. Anyways, this is how I solved it:

First, I subclass view A, I call the subclass ParentView. ParentView needs pointers to B1 and B2. I also add an additional pointer that is later used to store the currently selected view. Then, I override hitTest:

@interface ParentView : UIView
    @property (nonatomic, strong) UIView* viewB1;
    @property (nonatomic, strong) UIView* viewB2;
    @property (nonatomic, strong) UIView* currentlyTouchedView;
@end

@implementation ParentView

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    if (self.currentlyTouchedView == nil) {
        if (CGRectContainsPoint(self.viewB1.frame, point)) {
            self.currentlyTouchedView = viewB1;
        }else if(CGRectContainsPoint(self.viewB2.frame, point)) {
            self.currentlyTouchedView = viewB2;
        }
    }else {
        if (!CGRectContainsPoint(self.currentlyTouchedView.frame, point)) {
            return nil;
        }
    }

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

@end

So, what this code does is, it stores the view that is first touched. If one is already stored, it returns nil on hitTest, which eats up any additional touch events.

Now, where does currentlyTouchedView get set back to nil? For that, I subclass UIWindow and override sendEvent to check if all touches have ended:

@implementation EventOverrideWindow

- (void)sendEvent:(UIEvent *)event {
    [super sendEvent:event];
    BOOL allTouchesEnded = YES;
    for (UITouch* touch in event.allTouches) {
        if (touch.phase != UITouchPhaseCancelled && touch.phase != UITouchPhaseEnded) {
            allTouchesEnded = NO;
            break;
        }
    }

    if (allTouchesEnded) {
        ParentView* parentView = ((AppDelegate*)[UIApplication sharedApplication].delegate).parentView;

        parentView.currentlyTouchedView = nil;
    }
}

@end

I know this is not a very elegant solution and I'd appreciate if someone has a better idea.

Upvotes: 0

Arkku
Arkku

Reputation: 42109

Try setting both exclusiveTouch and multipleTouchEnabled to YES for B1 and B2.

Edit: I'm assuming from the wording “B1 should receive all events in its bounds” that you want to receive the touches directly in B1 & B2, not their subviews. If you want to restrict the touches to the B1/B2 bounds but handle them in the subviews I think you need to subclass B1 and B2, or A.

Edit 2: If it's the subviews of B1 and B2 that are interactive, you would need to set these properties to YES on all interactive subviews (recursively). But this also makes all subviews mutually exclusive to touch, not just between B1 and B2.

Upvotes: 1

jhabbott
jhabbott

Reputation: 19281

When B1 or B2 is touched, set exclusiveTouch to YES for that view and then set it back to NO again when the touches end. This means only the exclusive view will receive touches.

If you want to get touches outside the exclusive view's bounds you can store a reference to the view somewhere when you make it exclusive (myExclusiveView), then override hitTest:withEvent: for view A (or any superview) and have it return [myExclusiveView hitTest:withEvent:].

Upvotes: 0

Related Questions