Disable window interaction iOS - 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;
}

Related

How to remap a touch to another window?

I have an iPad with an external touchscreen. I am trying to remap the coordinate system of the external touchscreen to the UIWindow that is shown there.
I am getting the touches on the UIViewController of the UIWindow that is displayed on the iPad, as if I was touching the iPad. I do get them with the touchtype .stylus, which is how I can distinguish them from actual iPad touches (I will show different views on the iPad and the external screen). I am using the following code:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touchesOnExternalWindow = touches.filter({ $0.type == .stylus })
let touchesOnIpad = touches.subtracting(touchesOnExternalWindow)
if !touchesOnIpad.isEmpty {
super.touchesBegan(touchesOnIpad, with: event)
}
if !touchesOnExternalWindow.isEmpty {
guard let externalWindow = UIApplication.shared.windows.first(where: { $0.screen.bounds == screenBounds }) else {
fatalError("Touching the external display without an external display is not supported!")
}
externalWindow.rootViewController?.view.touchesBegan(touchesOnExternalWindow, with: event)
}
}
I am trying to pass the touches along to the second UIWindow, as if I were touching there. But that does not seem to work.
How can I touch a view programmatically? I am testing now with a button on the second screen, but it will need to work with SceneKit views as well.
You could have only one first responder at time and it can pass events only to one next responder.
So if you want to handle touches with stilus in external window, you need place this code inside into your iPad UIViewController next responder (it's not clear who next responder of your iPad UIViewController).
In the next responder of iPad UIViewController you place this code:
override var next: UIResponder? {
// The next responder of The UIResponder chain.
// It will receive responder events (like touches)
// that current responder (your iPad window UIViewController next reponder) did't handle.
guard let externalWindow = UIApplication.shared.windows.first(where: { $0.screen.bounds == screenBounds }) else {
fatalError("Touching the external display without an external display is not supported!")
}
return externalWindow
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touchesOnExternalWindow = touches.filter({ $0.type == .stylus })
let touchesOnIpad = touches.subtracting(touchesOnExternalWindow)
if !touchesOnIpad.isEmpty {
// Here you handle your touches on iPad, instead of passing it to next responder
}
// Pass touches to next responder(external window).
if !touchesOnExternalWindow.isEmpty {
super.touchesBegan(touchesOnExternalWindow, with: event)
}
}

Call hitTest inside of touchesMoved

I have an UIView which is on top of all other views and has overridden hitTest() method which always return itself:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
return self
}
Then, when I make some operations using points from touchesBegan(), I need to pass hitTest() to the views below of the our UIView:
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
// Do some operations
// ...
// ...
// ...
// pass touch event handling to views below or change hitTest()
}
So basically, on the top UIView I'm overriding touchesBegan(), touchesMoved() and touchesEnded() methods. Then I need to handle touches, perform some operations and then, if needed, to pass to views below. Is it possible?
It is probably simpler and better to solve your problem differently.
UIKit delivers a touch event by sending it to the window (the root of the view hierarchy) in a sendEvent(_:) message. The window's sendEvent(_:) method is responsible for finding the gesture recognizers interested in the touches, and sending the appropriate touchesBegan, touchesMoved, etc. messages to the recognizers and/or the hit view.
This means that you can subclass UIWindow and override sendEvent(_:) to get a look at every touch event in the window, before the event reaches any gesture recognizers or views, without overriding any view's hitTest(_:with:) method. Then you pass the event along to super.sendEvent(event) for normal routing.
Example:
class MyWindow: UIWindow {
override func sendEvent(_ event: UIEvent) {
if event.type == .touches {
if let count = event.allTouches?.filter({ $0.phase == .began }).count, count > 0 {
print("window found \(count) touches began")
}
if let count = event.allTouches?.filter({ $0.phase == .moved }).count, count > 0 {
print("window found \(count) touches moved")
}
if let count = event.allTouches?.filter({ $0.phase == .ended }).count, count > 0 {
print("window found \(count) touches ended")
}
if let count = event.allTouches?.filter({ $0.phase == .cancelled }).count, count > 0 {
print("window found \(count) touches cancelled")
}
}
super.sendEvent(event)
}
}
You can use this window subclass in your app by initializing your app delegate's window outlet to an instance of it, like this:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow? = MyWindow()
// other app delegate members...
}
Note that UIKit uses hitTest(_:with:) to set the view property of a touch when the touch begins, before it delivers the touch-began event to the window. UIKit also sets each touch's gestureRecognizers property to the set of recognizers that might want the touch (recognizer state .possible) or are actively using the touch (states began, changed, ended, cancelled) before passing the event to the window's sendEvent(_:). So your sendEvent(_:) override can look at each touch's view property if it needs to know where the touch is going.

Forward touches to UIPageViewController

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

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]

Resources