Forward touches to UIPageViewController - ios

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

Related

Pass UICollectionView touch event to its parent UITableViewCell

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.

Disable window interaction iOS

I have two UIWindows on the screen and one is behind the other.
is there a way to handle the user touch with the window that is behind?
Thanks
You should just be able to disable it like any UIView.
UIWindow *secondWindow = [[UIWindow alloc] initWithFrame:<#frame#>];
[secondWindow setUserInteractionEnabled:NO];
iOS looks by default first which window is touched, if multiple windows are in the touch area it picks the top one (based on window level, z index)
next it will call hitTest(_ point: CGPoint, with event: UIEvent) -> UIView? on the window that the os thinks is touched.
If your views in that window might return nil (no view could be touched in that window, or you have overridden the hitTest) then the window will return it self.
To solve this you will need to subclass UIWindow and override the hitTest function. If the view returned from the super.hitTest_:with:) is nil or self // the window then you might choose to delegate to another window. Pay attention to the fact that the window that was touched might have another coordinate space then the window that you will delegate to.
Example:
internal final class CustomWindow: UIWindow {
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let view = super.hitTest(point, with: event), view != self {
return view
}
guard let keyWindow = UIApplication.shared.keyWindow else {
return nil
}
return keyWindow.hitTest(keyWindow.convert(point, from: self), with: event)
}
}
You will only be able to accept touch events on one UIWindow at a time. The UIWindow that accepts events is called the keyWindow.
[behindWindow makeKeyAndVisible];
Your foreground UIWindow will remain visible, but your behindWindow will be receiving events.
Thanks, but i just found the way:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
for (UIWindow *win in [[UIApplication sharedApplication] windows]) {
if (win.tag != self.tag) {
return [win hitTest:point withEvent:event];
}
}
return nil;
}

How do I use hitTest:withEvent: to send touches from my nav bar to the underlying view under certain circumstances?

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)
}
}

Passing touches between stacked UIWindows?

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]

UIView: How to check if touches ended inside the same view in which they began

In AcaniUsers, I've created a grid of ThumbView : UIView instances inside of a UITableView. All thumbViews have a width of kThumbSize. How do I detect if touches ended inside the same view in which they began?
In the view extension you use;
Swift 4:
open override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
guard let touchPoint = touches.first?.location(in: self) else { return }
guard self.bounds.contains(touchPoint) else { return }
// Do items for successful touch inside the view
}
The following works, but I'm not sure if it's the best way to go about it. I think so though.
Since all thumbViews have a width of kThumbSize, just check in touchesEnded that the x-coordinate of the locationInView of the UITouch instance (assuming self.multipleTouchEnabled = NO) is less than or equal to kThumbSize. This means the touches ended inside the thumbView. No need to check the y-coordinate because if the touches move vertically, the tableView, which contains the thumbViews, scrolls and the touches are cancelled.
Do the following in ThumbView : UIView (whose instances are subviews of a UITableView):
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
NSLog(#"touchesEnded %#", touches);
CGPoint touchPoint = [[touches anyObject] /* only one */ locationInView:self];
if (touchPoint.x >= 0.0f && touchPoint.x <= kThumbSize) {
[(ThumbsTableViewCell *)self.superview.superview thumbTouched:self];
}
}
To only register touches on one thumbView at a time, you also probably want to set self.exclusiveTouch = YES; in the init instance method of ThumbView.

Resources