¿Por qué UINavigationBar roba eventos táctiles?

Tengo un UIButton personalizado con UILabel agregado como subvista. El botón realizar el selector dado solo cuando lo toco unos 15 puntos por debajo del límite superior. Y cuando toco sobre esa área, no pasa nada.

Descubrí que no ha sido causado por una creación incorrecta del botón y la etiqueta, porque después de que baje el botón a unos 15 px, funciona correctamente.

ACTUALIZAR Olvidé decir que el botón ubicado debajo de UINavigationBar y 1/3 de la parte superior del botón no reciben eventos táctiles.

La imagen estaba aquí

La vista con 4 botones se encuentra debajo de la Barra de navegación. Y cuando toque “Baloncesto” en la parte superior, BackButton obtendrá el evento táctil, y cuando toque “Piano” en la parte superior, a continuación, toqueBarButton derecho (si existe). Si no existe, no pasó nada.

No encontré esta característica documentada en los documentos de la aplicación.

También encontré este tema relacionado con mi problema, pero tampoco hay respuesta.

Noté que si estableces userInteractionEnabled en OFF, la barra de navegación ya no “roba” los toques.

Entonces debes subclasificar tu UINavigationBar y en tu CustomNavigationBar hacer esto:

-(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]; } 

Puede encontrar información sobre cómo subclase UINavigationBar aquí .

Encontré la respuesta aquí (Apple Developer Forum).

Keith en Apple Developer Technical Support, el 18 de mayo de 2010 (iPhone OS 3):

Le recomiendo que evite tener una IU sensible al tacto tan cerca de la barra de navegación o la barra de herramientas. Estas áreas se conocen como “factores de deslastre”, lo que facilita a los usuarios realizar eventos táctiles en los botones sin la dificultad de realizar toques de precisión. Este también es el caso de UIButtons por ejemplo.

Pero si desea capturar el evento táctil antes de que la barra de navegación o la barra de herramientas lo reciba, puede subclase UIWindow y anular: – (void) sendEvent: (UIEvent *) evento;

También descubrí que cuando toco el área debajo de UINavigationBar, la ubicación se define como 64, aunque no fue así. Así que hice esto:

CustomWindow.h

 @interface CustomWindow: UIWindow @end 

CustomWindow.m

 @implementation CustomWindow - (void) sendEvent:(UIEvent *)event { BOOL flag = YES; switch ([event type]) { case UIEventTypeTouches: //[self catchUIEventTypeTouches: event]; perform if you need to do something with event for (UITouch *touch in [event allTouches]) { if ([touch phase] == UITouchPhaseBegan) { for (int i=0; i< [self.subviews count]; i++) { //GET THE FINGER LOCATION ON THE SCREEN CGPoint location = [touch locationInView:[self.subviews objectAtIndex:i]]; //REPORT THE TOUCH NSLog(@"[%@] touchesBegan (%i,%i)", [[self.subviews objectAtIndex:i] class],(NSInteger) location.x, (NSInteger) location.y); if (((NSInteger)location.y) == 64) { flag = NO; } } } } break; default: break; } if(!flag) return; //to do nothing /*IMPORTANT*/[super sendEvent:(UIEvent *)event];/*IMPORTANT*/ } @end 

En la clase AppDelegate utilizo CustomWindow en lugar de UIWindow.

Ahora cuando toco el área debajo de la barra de navegación, no pasa nada.

Mis botones todavía no reciben eventos táctiles, porque no sé cómo enviar este evento (y cambiar las coordenadas) a mi vista con los botones.

Subclase UINavigationBar y agregue este método. Hará que se pasen los grifos a menos que estén tocando una subvista (como un botón).

  -(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *v = [super hitTest:point withEvent:event]; return v == self? nil: v; } 

Solo quería compartir otra perspectiva para resolver este problema. Esto no es un problema por diseño, pero estaba destinado a ayudar al usuario a volver o navegar. Pero tenemos que poner las cosas con fuerza dentro o debajo de la barra de navegación y las cosas se ven tristes.

Primero veamos el código.

 class MyNavigationBar: UINavigationBar { private var secondTap = false private var firstTapPoint = CGPointZero override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool { if !self.secondTap{ self.firstTapPoint = point } defer{ self.secondTap = !self.secondTap } return super.pointInside(firstTapPoint, withEvent: event) } 

}

Es posible que veas por qué estoy haciendo el manejo del segundo toque. Existe la receta para la solución.

La prueba de golpe se llama dos veces para una llamada. La primera vez que se informa el punto real en la ventana. Todo va bien. En el segundo pase, esto sucede.

Si el sistema ve una barra de navegación y el punto de golpe está alrededor de 9 píxeles más en el lado Y, intenta disminuirlo gradualmente a menos de 44 puntos, que es donde está la barra de navegación.

Mire la pantalla para ser claro.

enter image description here

Entonces hay un mecanismo que usará la lógica cercana al segundo pase de la prueba de golpe. Si podemos conocer su segundo pase y luego llamar al súper con el primer punto de prueba de golpe. Trabajo hecho.

El código anterior lo hace exactamente.

La solución para mí fue la siguiente:

Primero: agregue su aplicación (no importa dónde ingrese este código) una extensión para UINavigationBar como UINavigationBar : el siguiente código simplemente envía una notificación con el punto y el evento cuando se UINavigationBar navigationBar .

 extension UINavigationBar { open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { NotificationCenter.default.post(name: NSNotification.Name(rawValue: "tapNavigationBar"), object: nil, userInfo: ["point": point, "event": event as Any]) return super.hitTest(point, with: event) } } 

Luego, en su controlador de vista específico, debe escuchar esta notificación agregando esta línea en su viewDidLoad :

 NotificationCenter.default.addObserver(self, selector: #selector(tapNavigationBar), name: NSNotification.Name(rawValue: "tapNavigationBar"), object: nil) 

Entonces necesitas crear el método tapNavigationBar en tu controlador de vista así:

 func tapNavigationBar(notification: Notification) { let pointOpt = notification.userInfo?["point"] as? CGPoint let eventOpt = notification.userInfo?["event"] as? UIEvent? guard let point = pointOpt, let event = eventOpt else { return } let convertedPoint = YOUR_VIEW_BEHIND_THE_NAVBAR.convert(point, from: self.navigationController?.navigationBar) if YOUR_VIEW_BEHIND_THE_NAVBAR.point(inside: convertedPoint, with: event) { //Dispatch whatever you wanted at the first place. } } 

PD: No te olvides de eliminar la observación en el deinit así:

 deinit { NotificationCenter.default.removeObserver(self) } 

Eso es … Eso es un poco “complicado”, pero es una buena solución para no crear subclases y recibir una notificación cada vez que se usa la barra de navigationBar .

Hay 2 cosas que pueden estar causando problemas.

  1. ¿ setUserInteractionEnabled:NO para la etiqueta.

  2. Lo segundo que creo que podría funcionar es que después de agregar la etiqueta en la parte superior del botón puede enviar la etiqueta a la parte posterior (podría funcionar, aunque no estoy seguro)

    [button sendSubviewToBack:label];

Por favor, avíseme si el código funciona 🙂

Tus tags son enormes Comienzan en {0,0} (la esquina superior izquierda del botón), se extienden sobre todo el ancho del botón y tienen una altura de toda la vista. Verifique los datos de su frame y vuelva a intentarlo.

Además, tiene la opción de usar la propiedad titleLabel . Tal vez esté configurando el título más adelante y vaya a esta etiqueta en lugar de a su propia UILabel . Eso explicaría por qué el texto (que pertenece al botón) funcionaría, mientras que la etiqueta cubriría el rest del botón (sin dejar pasar los grifos).

titleLabel es una propiedad de solo lectura, pero puede personalizarla como su propia etiqueta (excepto tal vez el frame ) incluyendo el color del texto, la fuente, la sombra, etc.

Esto resolvió mi problema …

Añadí hitTest: withEvent: code a mi subclase de barra de navegación ..

  -(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { int errorMargin = 5;// space left to decrease the click event area CGRect smallerFrame = CGRectMake(0 , 0 - errorMargin, self.frame.size.width, self.frame.size.height); BOOL isTouchAllowed = (CGRectContainsPoint(smallerFrame, point) == 1); if (isTouchAllowed) { self.userInteractionEnabled = YES; } else { self.userInteractionEnabled = NO; } return [super hitTest:point withEvent:event]; } 

Extendiendo la solución de Alexander:

Paso 1. Subclase UIWindow

 @interface ChunyuWindow : UIWindow { NSMutableArray * _views; @private UIView *_touchView; } - (void)addViewForTouchPriority:(UIView*)view; - (void)removeViewForTouchPriority:(UIView*)view; @end // .m File // #import "ChunyuWindow.h" @implementation ChunyuWindow - (void) dealloc { TT_RELEASE_SAFELY(_views); [super dealloc]; } - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event { if (UIEventSubtypeMotionShake == motion && [TTNavigator navigator].supportsShakeToReload) { // If you're going to use a custom navigator implementation, you need to ensure that you // implement the reload method. If you're inheriting from TTNavigator, then you're fine. TTDASSERT([[TTNavigator navigator] respondsToSelector:@selector(reload)]); [(TTNavigator*)[TTNavigator navigator] reload]; } } - (void)addViewForTouchPriority:(UIView*)view { if ( !_views ) { _views = [[NSMutableArray alloc] init]; } if (![_views containsObject: view]) { [_views addObject:view]; } } - (void)removeViewForTouchPriority:(UIView*)view { if ( !_views ) { return; } if ([_views containsObject: view]) { [_views removeObject:view]; } } - (void)sendEvent:(UIEvent *)event { if ( !_views || _views.count == 0 ) { [super sendEvent:event]; return; } UITouch *touch = [[event allTouches] anyObject]; switch (touch.phase) { case UITouchPhaseBegan: { for ( UIView *view in _views ) { if ( CGRectContainsPoint(view.frame, [touch locationInView:[view superview]]) ) { _touchView = view; [_touchView touchesBegan:[event allTouches] withEvent:event]; return; } } break; } case UITouchPhaseMoved: { if ( _touchView ) { [_touchView touchesMoved:[event allTouches] withEvent:event]; return; } break; } case UITouchPhaseCancelled: { if ( _touchView ) { [_touchView touchesCancelled:[event allTouches] withEvent:event]; _touchView = nil; return; } break; } case UITouchPhaseEnded: { if ( _touchView ) { [_touchView touchesEnded:[event allTouches] withEvent:event]; _touchView = nil; return; } break; } default: { break; } } [super sendEvent:event]; } @end 

Paso 2: Asigna la instancia de ChunyuWindow instancia de AppDelegate

Paso 3: implementación de touchesEnded:widthEvent: para view con botones, por ejemplo:

 - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesEnded: touches withEvent: event]; UITouch *touch = [touches anyObject]; CGPoint point = [touch locationInView: _buttonsView]; // a subview contains buttons for (UIButton* button in _buttons) { if (CGRectContainsPoint(button.frame, point)) { [self onTabButtonClicked: button]; break; } } } 

Paso 4: llame a ChunyuWindow de addViewForTouchPriority cuando addViewForTouchPriority la vista que nos interese y llame a removeViewForTouchPriority cuando desaparezca la vista o dealloc, en viewDidAppear / viewDidDisappear / dealloc de ViewControllers, por lo que _touchView en ChunyuWindow es NULL, y es lo mismo que UIWindow , no tener efectos secundarios

Una solución alternativa que funcionó para mí, basada en la respuesta proporcionada por Alexandar:

 self.navigationController?.barHideOnTapGestureRecognizer.enabled = false 

En lugar de anular la UIWindow , puede desactivar el reconocedor de gestos responsable del “área de slop” en UINavigationBar .

Dar una versión de extensión de acuerdo con Bart Whiteley. No hay necesidad de subclase.

 @implementation UINavigationBar(Xxxxxx) - (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *v = [super hitTest:point withEvent:event]; return v == self ? nil: v; } @end