Here is the view structure:
The outer view is a UITableView.
Inside the UITableViewCell, there is a UICollectionView. And notice that there is some black spacing between the collection view cells.
When I tap the spacing in the UICollectionView, I want the touch event to pass to the UITableViewCell.
Excellent answer by #tounaobun. Tested and it works as expected:
1) If you tap anywhere on the collection view that is not an item, then the table cell underneath will select just fine.
2) if you tap on the items in the collection view, then the table cell will not select and you can interact with the collection view normally (aka, scrolling, invoking didSelectItem, etc)
I converted to swift for reference:
Swift 3:
class TableCellCollectionView: UICollectionView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let hitView = super.hitTest(point, with: event) {
if hitView is TableCellCollectionView {
return nil
} else {
return hitView
}
} else {
return nil
}
}
Just add this class definition to your code (I have it in a utility file) and then change the collection view's class from UICollectionView to TableCellCollectionView and you should be all set.
After google around, I have found a solution.Just inherit UICollectionView class and override hitTest:withEvent method.
CustomCollectionView.h
#interface CustomCollectionView : UICollectionView
#end
CustomCollectionView.m
#implementation CustomCollectionView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitView = [super hitTest:point withEvent:event];
if ([hitView isKindOfClass:[self class]]) {
// If it is class UICollectionView,just return nil.
return nil;
}
// else return super implementation.
return [super hitTest:point withEvent:event];
}
#end
Code from highest voted answer can be easily shortened.
Swift 4.1:
final class InteractiveCollectionView: UICollectionView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event) as? InteractiveCollectionView
}
}
You can override the point(inside:with:) method of UICollectionView and manually check whether any cell contains specified point:
class CollectionView: UICollectionView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
visibleCells.contains(where: { $0.frame.contains(point) })
}
}
Is that possible to set collectionView's backgroundView UserInteraction not enable, but make sure the collectionViewCell did, so that it could pass the tapEvent to next responder.
- (void)viewDidLoad {
theCollectionView.userInteractionEnabled = NO;
}
I hope it would help.
Related
I have a parent view which contains two elements. Essentially, they comprise a dropdown select. Referencing the below image: When the blue element is clicked, it shows an (initially hidden) dropdown UITableView. This UITableView is partially inside of the same parent view that also contains the blue element.
When I try to click on one of the UITableViewCells, only the first cell registers a touch event. If the table view is situated such that the first cell is partly inside of the parent, only clicks on the half of the image that is inside of the parent register.
This seems to be a hierarchy issue. I have tried:
Adjusting Z-indices
Situating the entire UITableView outside of the parent in the Storyboard hierarchy, but visually positioning it inside of it.
I'm not sure how to proceed.
EDIT:
See Andrea's answer which worked for me. I ended up overriding the point method as suggested, and did not use hitTest. However, I went with another implementation of the point method override:
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if super.point(inside: point, with: event) { return true }
for subview in subviews {
let subviewPoint = subview.convert(point, from: self)
if subview.point(inside: subviewPoint, with: event) { return true }
}
return false
}
As DonMag wrote:
You cannot interact with an element that extends beyond the bounds of
its parent (superview).
At least not without overriding some methods, views have their own implementations to detect touches. Most important methods are:
func hitTest(_ point: CGPoint,
with event: UIEvent?) -> UIView?
func point(inside point: CGPoint,
with event: UIEvent?) -> Bool
Those methods must be overriden, I've pasted some code on mine, is pretty old thus probably will require swift migration from 3.x to 4.x. Everything that you will add as a subview will handle touches even if is outside the bounds:
class GreedyTouchView: UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if !self.clipsToBounds && !self.isHidden && self.alpha > 0.0 {
let subviews = self.subviews.reversed()
for member in subviews {
let subPoint = member.convert(point, from: self)
if let result: UIView = member.hitTest(subPoint, with:event) {
return result
}
}
}
return super.hitTest(point, with: event)
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
return super.point(inside: point, with: event)
}
}
But I'd like to point out that drop down menus are more Web UI related and not iOS UI related, you should use a picker instead.
how can pass clicks and touch events through only a part of UIView?
The thing is that I have UIScrollView with transparent first element working as a spacing from the top and then I put some another views. I want that first element to pass clicks and touches to underlying views.
You can use hitTest to pass the touch events to view below.
Create a subclass of UIScrollView and override the hitTest
class MyScrollView: UIScrollView {
var underlyingViewReference : UIView!
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if underlyingViewReference.frame.contains(point) {
return underlyingViewReference
}
else {
return self
}
}
}
In the code above underlyingViewReference is the view to which you would like to handover the touch to.
If you dont wanna pass the reference of the view you can always declare protocol in UIScrollView and viewController loading UIScrollView can confirm to that protocol and later return the view as a return value :)
Hope it helps
I have a container view that contains the view of a UIPageViewController. This is inside a UIViewController and takes up the whole screen. On top of the container view I have a UIView, covering half the screen, which contains a button and some text. I want to forward the touches from this UIView to the UIPageViewController. This is so that the UIPageViewController can still be swiped left/right even if the user is swiping over the UIView. I also want the button to be able to be pressed, therefore can't just set isUserInteractionEnabled to false on the UIView.
How can I do this?
hitTest is the method which determines who should consume the touches/gestures.
So your "UIView, covering half the screen" can subclass from say NoTouchHandlerView like. And then this view will not consume touches. It would pass then to views under it.
class NoTouchHandlerView: UIView
{
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
{
if let hitTestView = super.hitTest(point, with: event), hitTestView !== self {
return hitTestView
}else {
return nil
}
}
}
Objective C version of the accepted answer for lazy guys like me :)
#implementation NoTouchHandlerView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView* hitTestView = [super hitTest:point withEvent:event];
if (hitTestView != nil && hitTestView != self) {
return hitTestView;
}
return nil;
}
#end
Long story short, if the alpha of my UINavigationBar is set to 0, I want all touches to go to the underlying view (which from the perspective of the view controller with the navigation bar would just be self.view). If it's 1.0, I want it to keep them.
I subclassed UINavigationBar, and I'm trying to override hitTest:withEvent:, but I'm really confused what I'm doing, and searching has been of little help.
How do I tell it, "if alpha is 0, send touch to self.view, otherwise keep on navigation bar"?
You will need to send a reference of your view into the navigation bar, set it as a property called behindView or something.
if(self.alpha == 0.0f)
{
return behindView;
}
return [super hitTest:point withEvent:event];
Another way would be to override pointInside:withEvent: like this:
if(self.alpha == 0.0f)
{
return NO;
}
return [super pointInside:point withEvent:event];
Swift 4 version.
Code:
class NavigationBar: UINavigationBar {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
if self.alpha == 0 {
return false
}
return super.point(inside: point, with: event)
}
}
I have two windows -- a front one, and a back one. The front one is used to overlay some stuff on top of the back one. I want to be able to capture touches on some parts of the front window, but not others; I want the front window to receive touches in some areas, but pass them through to the back window in others.
Any ideas as to how I'd do this?
Ok, here's what I did: I created two views in the front window. The first view covered the area where I wanted to catch the touches; the second, where I wanted the touches to pass through. I sub-classed UIWindow and overrode the hitTest:withEvent method like so:
- (UIView *) hitTest:(CGPoint)point withEvent:(UIEvent *)event {
// See if the hit is anywhere in our view hierarchy
UIView *hitTestResult = [super hitTest:point withEvent:event];
// ABKSlideupHostOverlay view covers the pass-through touch area. It's recognized
// by class, here, because the window doesn't have a pointer to the actual view object.
if ([hitTestResult isKindOfClass:[ABKSlideupHostOverlayView class]]) {
// Returning nil means this window's hierachy doesn't handle this event. Consequently,
// the event will be passed to the host window.
return nil;
}
return hitTestResult;
}
And in the class which creates the front window, I used a gesture recognizer to catch touches on the first view.
Same in 2017 code:
class PassView: UIView {}
class UIHigherWindow: UIWindow {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if hitView!.isKind(of: PassView.self) {
return nil
}
return hitView
}
}
Your "uber" window will be some view controller, UberVc.
Just make the main view (that is to say, simply the background .view) of UberVc be a PassView.
Then add say buttons etc. on the UberVc.
The above code results in any clicks on the buttons going to UberVc, and any clicks not on the buttons (ie, on the "background") going through to your regular window/VCs.
Just for clarity ...
If you simply want to pass everything through (so, the over-window is perhaps a message only or some sort of temporary thing), it's simply
class PassTroughWindow: UIWindow {
override func hitTest(_ p: CGPoint, with e: UIEvent?) -> UIView? {
return nil
}
}
and you're done.
I had a similar issue: the additional UIWindow should have a transparent view on it with some intractable subviews. All touches outside of those intractable views must be passed through.
I ended up using modified version of Anna's code that uses view's tag instead of type checking. This way view subclass creation is not required:
class PassTroughWindow: UIWindow {
var passTroughTag: Int?
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hitView = super.hitTest(point, with: event)
if let passTroughTag = passTroughTag {
if passTroughTag == hitView?.tag {
return nil
}
}
return hitView
}
}
Assuming you have a window and root view controller created you you could use it like this:
let window: PassTroughWindow
//Create or obtain window
let passTroughTag = 42
window.rootViewController.view.tag = passTroughTag
window.passTroughTag = passTroughTag
//Or with a view:
let untouchableView: UIView // Create it
untouchableView.tag = passTroughTag
window.addSubview(untouchableView)
Instead of using multiple UIWindows, get the key window with [[UIApplication sharedApplication] keyWindow], then add your view to it:
mainWindow = [[UIApplication sharedApplication] keyWindow];
[mainWindow addSubview:view]