I want to display a map view as a permanent background while other views are displayed on top of it (I'm going to set the alpha of the top view to something like 0.9 so the map is just faintly visible underneath) and at some points the map get revealed.
I have a container view which is layered on top of the map view and I would like to know if touch events that occur within the bounds of the container view can be passed to the map view so that it can be scrolled etc. Here's a sketch project showing an example of the architecture.
(The Container view is on top of the bottom half of the map view, the container view and contained View Controller's view's alphas are both 0, so to the user the map is visible on the entire screen).
Its easy to forward the touch events occurring within the Contained View Controller's views or child view controllers to the Map Background View Controller.
If I do something like pass the touch event to the map view like this
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
MapBackgroundViewController *parent = (MapBackgroundViewController *) self.parentViewController;
[parent.mapView touchesBegan:touches withEvent:event];
}
then nothing happens.
Is there a way of passing the touch events to the map view such that it will scroll etc.?
Yes, you can do this.
What I do is subclass UIView and override hitTest:withEvent: such that touches are passed through unless a subview is touched. Something like this:
#implementation PassthroughView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *view = [super hitTest:point withEvent:event];
return view == self ? nil : view;
}
#end
Then I assign this class to my container view and the contained view controller's main view in IB. So you can still interact with the content of the contained view controller, but touches on the container itself get passed through to the map.
You can pack your overlay views all into one container view, in which you then override -pointInside:withEvent: to return NO:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
return NO;
}
This will make the view effectively "untappable".
Alternatively you can also override -hitTest:withEvent: to return nil.
Simply setting userInteractionEnabled to NO won't work unfortunately, since the taps will still arrive in the view and be swallowed.
Related
I have a custom view that I present by tapping on either a button on the view controller or by tapping on a button on tableview cell (table view is a child of the view controller)
To dismiss the custom view I want the user to be able to tap anywhere on the screen to dismiss it. However due to the many hierarchies of the view in view controllers. A simple UITapGuestureRecognizer isn't working. Is there any workaround for a case like this?
Create a subclass of UIView, call it MyTapView. Assign this class your parent view, which holds all your subviews.
Override in your class the following to intercept any touches made to your view instance.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (!self.clipsToBounds && !self.hidden && self.alpha > 0) {
for (UIView *subview in self.subviews.reverseObjectEnumerator) {
CGPoint subPoint = [subview convertPoint:point fromView:self];
UIView *result = [subview hitTest:subPoint withEvent:event];
if (result != nil) {
return result;
}
}
}
// use this to pass the 'touch' onward in case no subviews trigger the touch
return [super hitTest:point withEvent:event];
}
This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01. This method does not take the view’s content into account when determining a hit (feel free to modify this). Thus, a view can still be returned even if the specified point is in a transparent portion of that view’s content and now, after it has been overridden, receives touches outside the bounds.
I have a UIViewController subclass whose view will generally contain some number of UIButtons and other interactive elements which may have one or more gesture recognizes attached to them.
What I'm trying to do is provide some visual feedback in the event that the user taps on a part of the screen that is not interactive. In other words: when the user taps the screen anywhere, if and only if no other control in the view responds to the touch event (including if it's, say, the start of a drag), then I want to fire off a method based on the location of the tap.
Is there a straightforward way to do this that would not require my attaching any additional logic to the interactive elements in the view, or that would at least allow me to attach such logic automatically by traversing the view hierarchy?
You can override the pointInside:withEvent: method of the container view and return NO if there is an interactive element under the tapped location, and return YES otherwise. The case where you return YES will correspond to tapping on an empty location.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
// Perform some checks using the CGRectContainsPoint method
// e.g. CGRectContainsPoint(oneOftheSubviews.frame, point)
}
A good alternative is using the hitTest:withEvent: method as in the example below:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *hitView = [super hitTest:point withEvent:event];
if (hitView != someView) return nil;
return hitView;
}
[super hitTest:point withEvent:event] will return the deepest view in that view's hierarchy that was tapped.
So you can check the type of hitView, and if it corresponds to one of your interactive subviews, return it. If it is equal to self, then it means that there isn't a subview under the tapped location, which in your case means that the user tapped on an empty area. In that case you'll do whatever you want to do and then return nil.
I have a view with a subview. When a button in the subview is tapped, the subview expands outside the bounds of a view, presenting couple of other buttons. However, I cannot find a way to interact with them.
I found a code at Apple's site:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// Convert the point to the target view's coordinate system.
// The target view isn't necessarily the immediate subview
CGPoint pointForTargetView = [self.targetView convertPoint:point fromView:self];
if (CGRectContainsPoint(self.targetView.bounds, pointForTargetView)) {
// The target view may have its view hierarchy,
// so call its hitTest method to return the right hit-test view
return [self.targetView hitTest:pointForTargetView withEvent:event];
}
return [super hitTest:point withEvent:event];
}
However, I cannot understand how should I use it, so that my subview will recognize the touches.
Any help would be greately appreciated.
You need to subclass the UIView or which ever class you need and override that method. Then create an object of that subclass and use it. It will then recognize the touches.
I've implemented a paged scroll according to this technique
( iOS develop. How to extend UIScrollView's scroll event responding area? ) and it works just as intended.
The view that I'm scrolling is containing a couple of buttons and I want to be able to click not only those that are centered/paged into the scrollview but also those to the left and to the right of it. I cannot find any way to solve this but I'm not really an iOS-Jedi yet, hoping one of you are though :)
So as you can see from the screenie the UIScrollView is about a third of the width of the window, the contentsize of the UIScrollView is much larger: about 1500px and contains a lot of buttons added programmatically. The cool thing with this solution, and the part that actually works, is that the buttons:
1) are paged into the scrollview
2) are visible outside the scrollview (since "clip subviews" is unchecked for the scrollview) 3) the buttons are clickable when visible inside the uiscrollview.
BUT what doesn't work is simply this:
- the buttons currently being outside of the window does not receive "their" clicks when clicking on them, the events are instead forwarded to the underlaying (the white part of the window) view.
So,
I finally managed to solve this puzzle and the solution is divided into two
parts. The problem was, as you way recall, that the click events did not travel to the
buttons that were(visible) outside the UIScrollView. It turned out that the clicks were captured by the underlying view and that it is possible to manipulate their way to finding their target by bending the rules a bit regarding who got hit and thereby tricking the events to get passed where you want them. Not really sure if this is how it should be done but it solved my problem.. . :)
1) First one must override the following method in the bottom view
so that it returns the scrollview instead of itself when appropriate.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *view = [super hitTest:point withEvent:event];
if (view == self)
return [self scrollView];
return view;
}
2) The scrollView must override TWO methods to hand over the clicks to its contained objects.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *view = [super hitTest:point withEvent:event];
// Always return us.
return view ;
}
and
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
// We want EVERYTHING!
return YES;
}
Thanks a lot for you comments and willingness to help.
I ho
Inspired by the answer #tommys mentioned, it turns out that by overriding the hinTest method of a UIView and return the scrollView instead, you actually can detach the swiping of this UIView to the scrollView.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *view = [super hitTest:point withEvent:event];
// Doing this make you detached the swiping to the scrollView
if (view == self)
return [self scrollView];
return view;
}
So this UIView is acting like an extension scroll area of the scrollView, the idea is here. If you make the UIView mask over the scrollView and same size of the window, then swiping anywhere inside the window makes the scrollView scroll.
Here is the example, ExtensionScrollArea
Here's my version:
hit test in container
- (UIView *) hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if ( CGRectContainsPoint( self.frame, point ) && ! self.hidden ) // <-- *
{
if ( ! CGRectContainsPoint( scrollView.frame, point ) )
return scrollView;
}
return [super hitTest:point withEvent:event];
}
(*) This marked line is important if you are moving about or otherwise hiding your view, for instance if you have multiple views, each with their own scrollviews. If you don't have this line, you may be directing all your touches to an off-screen scrollview!
override in scrollview
- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
return YES;
}
(in the hitTest of the container, you can exclude additional frames within the if statement for default behaviour) :)
I've got several cells containing views in this structure:
Main view
backview
frontview
The cells partly overlap. This means that the main view of cell A will partly cover the frontview of cell B. Like this:
B main view
B backview
B frontview
A main view
A backview
A frontview
I want to intercept touches on frontviews and backviews, but I want main views to ignore them.
(I've tried disabling user interaction on main views, but that also disables front and back views).
Any tips?
I found an answer here: http://vectorvector.tumblr.com/post/2130331861/ignore-touches-to-uiview-subclass-but-not-to-its
Basically, I'm making the main view a subclass of UIView, and overriding hitTest with this:
-(id)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
id hitView = [super hitTest:point withEvent:event];
if (hitView == self) return nil;
else return hitView;
}
(Note that confusingly you must set UserInteractionEnabled to true, ticked, yes for the UIView in question!)