cocoa-touch ipad uibarbuttonitem undo-redo uigesturerecognizer

cocoa touch - ¿Cómo se puede agregar un UIGestureRecognizer a un UIBarButtonItem como en el esquema común deshacer/rehacer UIPopoverController en las aplicaciones de iPad?



cocoa-touch undo-redo (13)

Problema

En mi aplicación para iPad, no puedo adjuntar un popover a un elemento de la barra de botones solo después de presionar y mantener presionados los eventos. Pero esto parece ser estándar para deshacer / rehacer. ¿Cómo hacen otras aplicaciones?

Fondo

Tengo un botón para deshacer (UIBarButtonSystemItemUndo) en la barra de herramientas de mi aplicación UIKit (iPad). Cuando presiono el botón Deshacer, se dispara su acción que es deshacer :, y que se ejecuta correctamente.

Sin embargo, la "convención UE estándar" para deshacer / rehacer en el iPad es que al presionar Deshacer se ejecuta un deshacer pero al presionar y mantener presionado el botón se muestra un controlador Popover donde el usuario selecciona "deshacer" o "rehacer" hasta que el controlador se descarta.

La forma normal de conectar un controlador de popover es con presentPopoverFromBarButtonItem :, y puedo configurar esto con la suficiente facilidad. Para que esto se muestre solo después de presionar y mantener, debemos configurar una vista para responder a eventos de gestos de "pulsación larga" como en este fragmento de código:

UILongPressGestureRecognizer *longPressOnUndoGesture = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPressOnUndoGesture:)]; //Broken because there is no customView in a UIBarButtonSystemItemUndo item [self.undoButtonItem.customView addGestureRecognizer:longPressOnUndoGesture]; [longPressOnUndoGesture release];

Con esto, después de presionar y mantener presionada la vista, se llamará al método handleLongPressOnUndoGesture: dentro de este método configuraré y mostraré el popover para deshacer / rehacer. Hasta aquí todo bien.

El problema con esto es que no hay vista para adjuntar. self.undoButtonItem es un UIButtonBarItem, no una vista.

Soluciones posibles

1) [El ideal] Adjunte el reconocedor de gestos al elemento de la barra de botones . Es posible adjuntar un reconocedor de gestos a una vista, pero UIButtonBarItem no es una vista. Tiene una propiedad para .customView, pero esa propiedad es nula cuando buttonbaritem es un tipo de sistema estándar (en este caso lo es).

2) Usa otra vista . Podría usar el UIToolbar, pero eso requeriría algunas pruebas de golpes extraños y sería un hack completo, incluso posible en primer lugar. No hay otra vista alternativa para usar que pueda pensar.

3) Use la propiedad de vista personalizada . Los tipos estándar como UIBarButtonSystemItemUndo no tienen customView (es nil). Configurar la vista personalizada borrará los contenidos estándar que necesita tener. Esto equivaldría a volver a implementar todo el aspecto y la función de UIBarButtonSystemItemUndo, nuevamente si es posible.

Pregunta

¿Cómo puedo adjuntar un reconocedor de gestos a este "botón"? Más específicamente, ¿cómo puedo implementar el estándar press-and-hold-to-show-redo-popover en una aplicación para iPad?

Ideas? Muchas gracias, especialmente si alguien realmente tiene esto trabajando en su aplicación (estoy pensando en ti, omni) y quiere compartir ...


En lugar de buscar una subvista, puede crear el botón por su cuenta y agregar un elemento de barra de botones con una vista personalizada. Luego conectas el GR a tu botón personalizado.


Esta es una pregunta antigua, pero aún aparece en las búsquedas de Google, y todas las otras respuestas son demasiado complicadas.

Tengo una barra de botones, con elementos de la barra de botones, que llaman a una acción: forEvent: método cuando se presiona.

En ese método, agregue estas líneas:

bool longpress=NO; UITouch *touch=[[[event allTouches] allObjects] objectAtIndex:0]; if(touch.tapCount==0) longpress=YES;

Si fue un solo toque, tapCount es uno. Si fue un doble toque, tapCount es dos. Si es una pulsación larga, tapCount es cero.


Hasta iOS 11, let barbuttonView = barButton.value(forKey: "view") as? UIView let barbuttonView = barButton.value(forKey: "view") as? UIView nos dará la referencia a la vista de barButton en la que podemos agregar gestos fácilmente, pero en iOS 11 las cosas son bastante diferentes, la línea de código anterior terminará en nula, por lo que se agrega un gesto de toque a la vista para "ver" " no tiene sentido.

No se preocupe, podemos agregar gestos de toque en UIBarItems, ya que tiene una propiedad customView. Lo que podemos hacer es crear un botón con altura y ancho de 24 pt (de acuerdo con las pautas de interfaz humana de Apple) y luego asignar la vista personalizada como el botón recién creado. El siguiente código te ayudará a realizar una acción para un toque y otra para tocar el botón de la barra 5 veces.

NOTA Para este propósito, ya debe tener una referencia al barbuttonitem.

func setupTapGestureForSettingsButton() { let multiTapGesture = UITapGestureRecognizer() multiTapGesture.numberOfTapsRequired = 5 multiTapGesture.numberOfTouchesRequired = 1 multiTapGesture.addTarget(self, action: #selector(HomeTVC.askForPassword)) let button = UIButton(frame: CGRect(x: 0, y: 0, width: 24, height: 24)) button.addTarget(self, action: #selector(changeSettings(_:)), for: .touchUpInside) let image = UIImage(named: "test_image")withRenderingMode(.alwaysTemplate) button.setImage(image, for: .normal) button.tintColor = ColorConstant.Palette.Blue settingButton.customView = button settingButton.customView?.addGestureRecognizer(multiTapGesture) }


Intenté algo similar a lo que sugirió Ben. Creé una vista personalizada con un UIButton y la usé como vista personalizada para UIBarButtonItem. Hubo un par de cosas que no me gustó acerca de este enfoque:

  • El botón necesitaba ser diseñado para no sobresalir como un pulgar dolorido en la UIToolBar
  • Con un UILongPressGestureRecognizer, parece que no obtuve el evento click para "Touch Up Inside" (Esto podría / probablemente es un error de programación de mi parte).

En cambio, me conformé con algo hackish en el mejor de los casos, pero a mí me funciona. Estoy usando XCode 4.2 y estoy usando ARC en el siguiente código. Creé una nueva subclase UIViewController llamada CustomBarButtonItemView. En el archivo CustomBarButtonItemView.xib, creé una UIToolBar y agregué un solo UIBarButtonItem a la barra de herramientas. Luego reduje la barra de herramientas a casi el ancho del botón. Luego conecté la propiedad de vista Propietario del archivo a la UIToolBar.

Luego, en ViewDidLoad de mi ViewController: mensaje, creé dos UIGestureRecognizers. El primero fue un UILongPressGestureRecognizer para hacer clic y mantener y el segundo fue UITapGestureRecognizer. Parece que no puedo obtener correctamente la acción para el UIBarButtonItem en la vista, así que lo falsifico con el UITAPGestureRecognizer. UIBarButtonItem se muestra a sí mismo como hecho clic y el UITAPGestureRecognizer se ocupa de la acción como si se hubiera establecido la acción y el objetivo para UIBarButtonItem.

- (void)viewDidLoad { [super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPressGestured)]; UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(buttonPressed:)]; CustomBarButtomItemView* customBarButtonViewController = [[CustomBarButtomItemView alloc] initWithNibName:@"CustomBarButtonItemView" bundle:nil]; self.barButtonItem.customView = customBarButtonViewController.view; longPress.minimumPressDuration = 1.0; [self.barButtonItem.customView addGestureRecognizer:longPress]; [self.barButtonItem.customView addGestureRecognizer:singleTap]; } -(IBAction)buttonPressed:(id)sender{ NSLog(@"Button Pressed"); }; -(void)longPressGestured{ NSLog(@"Long Press Gestured"); }

Ahora, cuando se produce un solo clic en BarButtonItem de ViewController (Conectado a través del archivo xib), el gesto de tocar llama al botónPresionado: mensaje. Si el botón se mantiene presionado, longPressGestured se activa.

Para cambiar la apariencia del UIBarButton, sugeriría crear una propiedad para CustomBarButtonItemView para permitir el acceso al Custom BarButton y almacenarlo en la clase ViewController. Cuando se envía el mensaje longPressGestured, puede cambiar el ícono del sistema del botón.

Una gotcha que encontré es que la propiedad de vista personalizada toma la vista tal como está. Si modifica el UIBarButtonitem personalizado de CustomBarButtonItemView.xib para cambiar la etiqueta a @ "cadena realmente larga", por ejemplo, el botón se redimensionará a sí mismo, pero solo la parte más a la izquierda del botón se muestra en la vista que está viendo las instancias de UIGestuerRecognizer.


La opción 1 es de hecho posible. Desafortunadamente, es doloroso encontrar el UIView que crea el UIBarButtonItem. Así es como lo encontré:

[[[myToolbar subviews] objectAtIndex:[[myToolbar items] indexOfObject:myBarButton]] addGestureRecognizer:myGesture];

Esto es más difícil de lo que debería ser, pero está claramente diseñado para evitar que la gente bromee con la apariencia y el tacto de los botones.

Tenga en cuenta que los espacios fijos / flexibles no se cuentan como vistas.

Para manejar espacios debes tener alguna forma de detectarlos, y lamentablemente el SDK simplemente no tiene una manera fácil de hacerlo. Hay soluciones y estas son algunas de ellas:

1) Establezca el valor de la etiqueta UIBarButtonItem en su índice de izquierda a derecha en la barra de herramientas. Esto requiere demasiado trabajo manual para mantenerlo sincronizado con la OMI.

2) Establezca la propiedad habilitada de cualquier espacio en NO. A continuación, utilice este fragmento de código para establecer los valores de etiqueta para usted:

NSUInteger index = 0; for (UIBarButtonItem *anItem in [myToolbar items]) { if (anItem.enabled) { // For enabled items set a tag. anItem.tag = index; index ++; } } // Tag is now equal to subview index. [[[myToolbar subviews] objectAtIndex:myButton.tag] addGestureRecognizer:myGesture];

Por supuesto, esto tiene un riesgo potencial si desactiva un botón por alguna otra razón.

3) Codifique manualmente la barra de herramientas y maneje los índices usted mismo. Como usted mismo construirá el UIBarButtonItem, sabrá de antemano qué índice serán en las subvistas. Puede ampliar esta idea para recopilar el UIView de antemano para su uso posterior, si es necesario.


La segunda opción de @ voi1d es la más útil para aquellos que no desean reescribir toda la funcionalidad de UIBarButtonItem . Envolví esto en una categoría para que puedas hacer:

[myToolbar addGestureRecognizer:(UIGestureRecognizer *)recognizer toBarButton:(UIBarButtonItem *)barButton];

con un pequeño error de manejo en caso de que esté interesado. NOTA: cada vez que agregue o elimine elementos de la barra de herramientas utilizando setItems , tendrá que volver a agregar cualquier reconocedor de gestos, supongo que UIToolbar recrea los UIViews retenidos cada vez que ajusta la matriz de elementos.

UIToolbar + Gesture.h

#import <UIKit/UIKit.h> @interface UIToolbar (Gesture) - (void)addGestureRecognizer:(UIGestureRecognizer *)recognizer toBarButton:(UIBarButtonItem *)barButton; @end

UIToolbar + Gesture.m

#import "UIToolbar+Gesture.h" @implementation UIToolbar (Gesture) - (void)addGestureRecognizer:(UIGestureRecognizer *)recognizer toBarButton:(UIBarButtonItem *)barButton { NSUInteger index = 0; NSInteger savedTag = barButton.tag; barButton.tag = NSNotFound; for (UIBarButtonItem *anItem in [self items]) { if (anItem.enabled) { anItem.tag = index; index ++; } } if (NSNotFound != barButton.tag) { [[[self subviews] objectAtIndex:barButton.tag] addGestureRecognizer:recognizer]; } barButton.tag = savedTag; } @end


Probé la solución de @ voi1d, que funcionó muy bien hasta que cambié el título del botón al que le había agregado un gesto de prensa prolongado. Al cambiar el título aparece una nueva UIView para el botón que reemplaza al original, lo que hace que el gesto agregado deje de funcionar tan pronto como se realiza un cambio en el botón (lo que sucede con frecuencia en mi aplicación).

Mi solución fue subclasificar UIToolbar y anular el método addSubview: También creé una propiedad que mantiene el puntero al objetivo de mi gesto. Aquí está el código exacto:

- (void)addSubview:(UIView *)view { // This method is overridden in order to add a long-press gesture recognizer // to a UIBarButtonItem. Apple makes this way too difficult, but I am clever! [super addSubview:view]; // NOTE - this depends the button of interest being 150 pixels across (I know...) if (view.frame.size.width == 150) { UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:targetOfGestureRecognizers action:@selector(showChapterMenu:)]; [view addGestureRecognizer:longPress]; } }

En mi situación particular, el botón en el que estoy interesado tiene 150 píxeles de ancho (y es el único que lo es), así que esa es la prueba que uso. Probablemente no sea la prueba más segura, pero funciona para mí. Obviamente tendrías que hacer tu propia prueba y proporcionar tu propio gesto y selector.

El beneficio de hacerlo de esta manera es que cada vez que mi UIBarButtonItem cambia (y así crea una nueva vista), mi gesto personalizado se adjunta, ¡así que siempre funciona!


Respuesta @utopians en Swift 4.2

@objc func myAction(_ sender: UIBarButtonItem, forEvent event:UIEvent) { let longPressed:Bool = (event.allTouches?.first?.tapCount).map {$0 == 0} ?? false ... handle long press ... }


Sé que esto es viejo, pero pasé una noche golpeando mi cabeza contra la pared tratando de encontrar una solución aceptable. No quería usar la propiedad customView porque eliminaría todas las funciones integradas como el tinte del botón, el tinte desactivado, y la pulsación larga estaría sujeta a un cuadro de hit tan pequeño, mientras que UIBarButtonItems extendió su cuadro de hit bastante formas. Se me ocurrió esta solución que creo que funciona muy bien y es solo un pequeño dolor de implementar.

En mi caso, los primeros 2 botones de mi barra irían al mismo lugar si se presionan durante mucho tiempo, así que solo necesitaba detectar que una pulsación sucedió antes de un cierto punto X. Agregué el reconocedor de gestos de pulsación larga a la UIToolbar (también funciona si lo agregas a una UINavigationBar) y luego agregué un UIBarButtonItem extra de 1 píxel de ancho justo después del 2º botón. Cuando se carga la vista, agrego una UIView que tiene un solo píxel de ancho para ese UIBarButtonItem como es customView. Ahora puedo probar el punto donde sucedió la pulsación larga y luego ver si es X menos que la X del marco de la vista personalizada. Aquí hay un pequeño código de Swift 3

@IBOutlet var thinSpacer: UIBarButtonItem! func viewDidLoad() { ... let thinView = UIView(frame: CGRect(x: 0, y: 0, width: 1, height: 22)) self.thinSpacer.customView = thinView let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPressed(gestureRecognizer:))) self.navigationController?.toolbar.addGestureRecognizer(longPress) ... } func longPressed(gestureRecognizer: UIGestureRecognizer) { guard gestureRecognizer.state == .began, let spacer = self.thinSpacer.customView else { return } let point = gestureRecognizer.location(ofTouch: 0, in: gestureRecognizer.view) if point.x < spacer.frame.origin.x { print("Long Press Success!") } else { print("Long Pressed Somewhere Else") } }

Definitivamente no es ideal, pero es lo suficientemente fácil para mi caso de uso. Si necesita especificar una pulsación larga en botones específicos en ubicaciones específicas, se vuelve un poco más molesto, pero debería poder rodear los botones que necesita para detectar la pulsación larga con espaciadores finos y luego simplemente verificar que la X de su punto sea entre ambos espaciadores.


Sé que no es la mejor solución, pero voy a publicar una solución bastante fácil que funcionó para mí.

He creado una extensión simple para UIBarButtonItem :

fileprivate extension UIBarButtonItem { var view: UIView? { return value(forKey: "view") as? UIView } func addGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) { view?.addGestureRecognizer(gestureRecognizer) } }

Después de esto, puede simplemente agregar sus reconocedores de gestos a los elementos en el método viewDidLoad su ViewController:

@IBOutlet weak var myBarButtonItem: UIBarButtonItem! func setupLongPressObservation() { let recognizer = UILongPressGestureRecognizer( target: self, action: #selector(self.didLongPressMyBarButtonItem(recognizer:))) myBarButtonItem.addGestureRecognizer(recognizer) }


Si bien esta pregunta tiene más de un año, este sigue siendo un problema bastante molesto. He enviado un informe de error a Apple ( rdar: // 9982911 ) y sugiero que cualquier otra persona que lo sienta lo duplique.


También puedes simplemente hacer esto ...

let longPress = UILongPressGestureRecognizer(target: self, action: "longPress:") navigationController?.toolbar.addGestureRecognizer(longPress) func longPress(sender: UILongPressGestureRecognizer) { let location = sender.locationInView(navigationController?.toolbar) println(location) }


Nota: esto ya no funciona a partir de iOS 11

En lugar de ese lío al tratar de encontrar la vista de UIBarButtonItem en la lista de subvistas de la barra de herramientas, también puede probar esto, una vez que el elemento se agrega a la barra de herramientas:

[barButtonItem valueForKey:@"view"];

Utiliza el marco de codificación de clave-valor para acceder a la variable _view privada de UIBarButtonItem, donde mantiene la vista que creó.

De acuerdo, no sé dónde cae esto en términos de la API privada de Apple (este es un método público utilizado para acceder a una variable privada de una clase pública, no como acceder a marcos privados para crear efectos exclusivos de Apple o cualquier cosa), pero funciona, y sin dolor.