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]
Related
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.
I have a view in my storyboard that by default the alpha is set to 0. In certain cases the Swift file sets the alpha to 1. So either hidden or not. Before this view just contained 2 labels. I'm trying to add 2 buttons to the view.
For some reason the buttons aren't clickable at all. So when you tap it normally buttons change color slightly before you releasing, or while holding down on the button. But that behavior doesn't happen for some reason and the function connected to the button isn't being called at all.
It seems like an issue where something is overlapping or on top of the button. The button is totally visible and enabled and everything but not clickable. I tried Debug View Hierarchy but everything looks correct in that view.
Any ideas why this might be happening?
EDIT I tried making a class with the following code and in interface builder setting the container view to be that class.
class AnotherView: UIView {
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
for view in self.subviews {
if view.isUserInteractionEnabled, view.point(inside: self.convert(point, to: view), with: event) {
return true
}
}
return false
}
}
Go with hitTest(_:with:) method. When we call super.hitTest(point, with: event), the super call returns nil, because user interaction is disabled. So, instead, we check if the touchPoint is on the UIButton, if it is, then we can return UIButton object. This sends message to the selector of the UIButton object.
class AnotherView: UIView {
#IBOutlet weak var button:UIButton!
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let view = super.hitTest(point, with: event)
if self.button.frame.contains(point) {
return button
}
return view
}
#IBAction func buttnTapped(sender:UIButton) {
}
}
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
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;
}
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)
}
}