2012-01-31 24 views
16

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.¿Por qué UINavigationBar roba eventos táctiles?

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.

ACTUALIZACIÓN 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.

Image was here

Vista con 4 botones se encuentra bajo la navigationbar. 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é this tema relacionado con mi problema, pero tampoco hay ninguna respuesta.

+0

Una respuesta de @nonamelive en este enlace resuelve el problema en todos los sistemas operativos (incluido iOS 7.0/7.1): http://stackoverflow.com/questions/7806557/touch-events-within-8-pixels-of-nav -bar-not-called – strange

Respuesta

16

descubrí la respuesta here (Apple Foro de Desarrolladores).

Keith en Apple Developer Soporte Técnico, el 18 de mayo de 2010 (iPhone OS 3):

recomiendo que evite tener la interfaz de usuario sensible al tacto con tanta proximidad a la barra de navegación o 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 lo era. Hice, pues, la siguiente:

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 aún no reciben eventos táctiles, porque no sé cómo enviar este evento (y cambiar las coordenadas) a mi vista con los botones.

+1

¿Sabes qué es lo más extraño aquí? Que en el iPad, en lugar de 64, el origen que debería estar marcado como NO es 44 ... –

+1

Quizás esto sea así. No he comprobado en el iPad. Y sí, esto es raro. – Alexander

+0

Esto sucede incluso fuera de los límites del controlador de vista contenedor ([mi pregunta] (http://stackoverflow.com/questions/27992243/tap-region-issues-with-uinavigationcontroller-in-vc-containment/)). He archivado esto como radar 19504573. –

0

Hay 2 cosas que pueden estar causando problemas.

  1. ¿Has probado setUserInteractionEnabled:NO para la etiqueta.

  2. segunda cosa que creo que podría funcionar es aparte de eso después de añadir la etiqueta en la parte superior del botón puede enviar la etiqueta hacia atrás (que podría funcionar, no está seguro aunque)

    [button sendSubviewToBack:label];

Por favor, hágamelo saber si el código funciona :)

+0

Gracias, pero el código funciona de la misma manera, el área sobre el texto de la etiqueta no recibe toques ... pero bajo el texto de la etiqueta funciona normalmente. – Alexander

0

Sus etiquetas 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 su información de frame y vuelva a intentarlo.

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

titleLabel es una propiedad de sólo lectura, pero se puede personalizar al igual que su propio sello (excepto quizás el frame) incluyendo el color del texto, la fuente, sombra, etc.

+0

Gracias, pero descubrí que ese no era el problema del botón y la etiqueta de creación. Y ahora uso titleLabel en lugar de mi etiqueta. – Alexander

27

Me di cuenta de que si configura userInteractionEnabled en OFF, la barra de navegación ya no "roba" los toques.

Así que hay que subclase su UINavigationBar y en su 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]; 
} 

información acerca de cómo crear subclases UINavigationBar puede encontrar here.

+1

Esto solo funciona en el simulador –

+1

Parece que la desactivación de la interacción del usuario funciona en mi dispositivo. Estoy probando con un iPhone 5 y iOS 6.1.2. Subclases no es necesario sin embargo; puede desactivar la interacción del usuario externamente. – jtbandes

+0

muy buena solución, ya que no puedo deshabilitar la interacción del usuario (había un botón), gracias. –

0

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: Asignar ChunyuWindow ejemplo a AppDelegate Instancia

Paso 3: Implementar 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: llamar ChunyuWindow 's addViewForTouchPriority cuando la vista nos preocupa aparece, y llamar a removeViewForTouchPriority cuando la vista desaparece o dealloc, en viewDidAppear/viewDidDisappear/dealloc de ViewControllers, por lo _touchView en ChunyuWindow es nulo, y es la misma como UIWindow, sin efectos secundarios.

0

Esto resolvió mi problema ..

añadí hitTest:withEvent: código a mi subclase 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]; 
    } 
3

Subclase UINavigationBar y añadir a 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; 
} 
+0

Una manera limpia e inteligente. Funciona para mí –

+0

Notablemente, esto romperá cualquier 'UIBarButtonitem's no personalizado en su barra de navegación, ej. el botón Atrás. – mxcl

1

Solo quería compartir otra posibilidad 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) 
} 

}

Usted puede ser por qué estoy haciendo segunda manejo táctil. Existe la receta para la solución.

La prueba de hit 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.

Eche un vistazo a la pantalla para que quede claro.

enter image description here

así que hay un mecanismo que va a utilizar la lógica cercana a la segunda fase de hittest. 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.

+0

si la prueba de golpe se llama un número impar de veces por alguna razón, entonces se rompería, ¿no? –

0

Una solución alternativa que trabajó para mí, sobre la base de la respuesta proporcionada por Alexandar:

self.navigationController?.barHideOnTapGestureRecognizer.enabled = false 

En lugar de anular la UIWindow, sólo puede desactivar el reconocedor gesto responsable de la "zona de decantación" en la UINavigationBar .

+2

parecía prometedor pero no funcionaba (ios10, probado en un dispositivo) –

+0

Salvador, la solución más simple, luchó todo el día por esto, gracias Chris. – skkrish

+0

Tampoco funciona en el simulador iOS 9. – Greg

0

Proporcione una versión de extensión según 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 
+0

No es seguro agregar métodos de categoría con el mismo nombre que los métodos existentes en un objeto. (ver https://stackoverflow.com/a/5272612/42484) – Greg

0

La solución para mí fue la siguiente:

Primero: añadir en su aplicación (No importa donde se introduce el código) una extensión para UINavigationBar así: El siguiente código solo envíe una notificación con el punto y el evento cuando se está tomando el 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 el controlador de vista específico que debe escuchar a esta notificación mediante la adición de esta línea en su viewDidLoad:

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

Luego hay que crear el método tapNavigationBar en su controlador de vista como tan:

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 quitar la observación en el deinit así:

deinit { 
    NotificationCenter.default.removeObserver(self) 
} 

Eso es todo ...Eso es un poco "complicado", pero es una buena solución para no crear subclases y recibir una notificación en cualquier momento que se toque el navigationBar.

Cuestiones relacionadas