I have a case where a transparent UIViewController is presented over a map view.
ViewController.view is transparent and it's a "pass-through" view. This means that all of ViewController's touches are passed to the map view that is behind the VC.
"Pass through" behaviour is implemented by
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for subview in subviews {
if !subview.isHidden && subview.alpha > 0 && subview.isUserInteractionEnabled && subview.point(inside: convert(point, to: subview), with: event) {
return true
}
}
return false
}
All touches and gestures work perfectly, but I found that VoiceOver doesn't work at all. MapView isn't VO accessible. I can't select any mapView element with VO.
I did some research and found that VO uses a private method:
[UIViewAccessibility(SafeCategory) _accessibilityHitTest:withEvent:]
And looks like this method doesn't use the overridden pointInside method. So does anyone know how to fix it in a safe way?
You can make VoiceOver ignore the view by setting view.isAccessibilityElement = NO;.
Alternately, you can make view conform to UIAccessibilityContainer protocol. If for some reason that doesn’t work, you can set view.accessibilityElements = #[mapView, ...];
Related
How do I allow my tableView to be selectable by overriding hitTest?
I have a tableview that lives outside of bounds of its superview so I need to override hitTest of the view containing the superview. However, when I pass the tableview's hittest I can only scroll and not tap on a row.
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let pointInTableview = tableView.convert(point, from: self)
guard tableView.bounds.contains(pointInTableview) else {
return super.hitTest(point, with: event)
}
return tableView.hitTest(pointInTableview, with: event)
}
The datasource and delegates are set. If I manually call tableview.selectRowAt(..) I am able to receive the delegate callback.
Gif of demo
The VC View is the view that is overriding hitTest. We are trying to pass the touches to the tableview because the tableview is outside the bounds of the view it lives in.
The problem wasn't the hitTest.
My VC View had a tap gesture to close keyboards. This tap gesture was cancelling the tableview's tap gesture.
Ref: https://kakubei.github.io/2016/02/24/Tap-Gesture-and-TableView/
I'm building a collectionview. Below of it I placed some buttons as shown in the picture.
What I want is to make the UICollectionView background pass taps below, so the desired buttons can receive taps.
I don't need to add Tap gesture recognizers to the background view (the problem I'm describing is just an example here), I need the buttons' actuons to be triggered directly when they're tapped.
I thought I could do this by making the background clear or disabling user interaction for the background view. While disabling it for the entire collection view works, this other way does not.
How can I make the background view of my collectionView be "invisible" so that taps go straight to the below buttons instead of going to the collectionview background?
The following is an example of my layout.
Assuming your collectionView and your buttons share the same superview, this should do the trick.
What you want to do is bypass the backgroundView and forward hits to the subviews underneath the collectionView.
Notice that we are picking the last subview with the matching criteria. That is because the last subview in the array is the closest to the user's finger.
class SiblingAwareCollectionView: UICollectionView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let hit = super.hitTest(point, with: event)
guard hit === backgroundView else {
return hit
}
let sibling = superview?
.subviews
.filter { $0 !== self }
.filter { $0.canHit }
.last { $0.point(inside: convert(point, to: $0), with: event) }
return sibling ?? hit
}
}
If you look at the documentation for hitTest(_:with:) it says:
This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01.
For convenience, here is an extension to ensure we are playing by the rules:
extension UIView {
var canHit: Bool {
!isHidden && isUserInteractionEnabled && alpha >= 0.01
}
}
I have a layout with a UIView at the top of the page and, right below it, I have a UITableView.
What I am wanting to do is to transfer the gesture interactions on the UIView to the UITableView, so when the user makes a drag up/down on the UIView, the UITableView scrolls vertically.
I tried the following code
tableView.gestureRecognizers?.forEach { uiView.addGestureRecognizer($0) }
but it removed the gestureRecognizers from the UITableView somehow :/
Obs.: the UIView cannot be a Header of the UIScrollView
That's Tricky
What is problem ?
Your top view is not allowed to pass through view behind it...
What would be possible solutions
pass all touches to view behind it (Seems to not possible or very tough practically )
Tell window to ignore touches on top view (Easy one)
Second option is better and easy.
So What you need to do is create subclass of UIView and override
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
and return nil if you found same view on hitTest action
Here Tested and working example
class PassThroughME : UIView {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return super.hitTest(point, with: event) == self ? nil : self
}
}
That's it now use PassThroughME either by adding class to your view in storyboard or programmatically whatever way you have added your view
Check image i have black color view with 0.7 alpha on top still i am able to scroll
Hope it is helpful
Issue: The viewWithGesture contains the viewUserSees, and is draggable within the blue containerView. However, the viewWithGesture is a subView of the containerView, so when the viewWithGesture is at an extreme (illustrated here - half in and half out of the containerView), only half of the viewWithGesture responds to touches, making it very hard to drag.
Note: I realize I should redo all the math that keeps it in the container and move it outside of the containerView, but I am very curious to learn how to do this the "worse" way.
I have researched this a bunch and tried to implement hittest() and pointInside(), but so far I have managed to just make the app crash spectacularly.
Is there a good, relatively clean way to let the user grab from outside the containerView? (swift3 if possible)
EDIT: The green box is transparent and half of it is in the containerView and half is not.
In order for a view to receive a touch, the view and all its ancestors must return true from pointInside:withEvent:.
Normally, pointInside:withEvent: returns false if the point is outside the view's bounds. Since a touch in the green area is outside the container view's bounds, the container view returns false, so the touch won't hit the gesture view.
To fix this, you need to create a subclass for the container view and override its pointInside:withEvent:. In your override, return true if the point is in the container view's bounds or in the gesture view's bounds. Perhaps you can be lazy (especially if your container view doesn't have many subviews) and just return true if the point is in any subview's bounds.
class ContainerView: UIView {
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
}
}
I have a strong feeling that this might be a bug with Xcode that apple just needs to fix. I have a view controller that is embedded in a navigation controller as the root controller. The view controller has a button that is vertically aligned with the top layout guide. The view controller's button works fine and all of the tappable area is working.
However, if I push the same or a similar view controller on to the navigation stack, it's button will not be completely tappable. The top of the button (about 10 pixels or so) is no longer tappable. If I try to tap at the top left of the button, the back button on the navigation bar is tapped instead, even though I am clearly not tapping the navigation bar bounds. I assume this is a bug with apple but I was wondering if anyone knows of a fix. Here is the link to the github project if anyone needs it.
This is the default behavior for iOS. Many UIViews have this extended touch functionality in iOS. For example, UINavigationBar, UITabBar, UISecgmentedControl etc. I believe this is to make easer touch to these controls.
If you still want to override this default behavior. You can do this by subclassing UINavigationBar and add this method in the subclass:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([self pointInside:point withEvent:event]) {
self.userInteractionEnabled = YES;
} else {
self.userInteractionEnabled = NO;
}
return [super hitTest:point withEvent:event];
}
A merge request to your Github project is made.
Here's a swift version of the solution in case anyone needs it:
class CustomNavBar: UINavigationBar {
override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
if pointInside(point, withEvent: event) {
userInteractionEnabled = true
} else {
userInteractionEnabled = false
}
return super.hitTest(point, withEvent: event)
}
}
Swift 3 and higher:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
isUserInteractionEnabled = self.point(inside: point, with: event)
return super.hitTest(point, with: event)
}