ios objective-c uicontrol

ios - ¿Cómo subclasificar correctamente UIControl?



objective-c (7)

Obj C

Encontré un artículo relacionado de 2014 https://www.objc.io/issues/13-architecture/behaviors/ .

Lo interesante de esto es que su enfoque es hacer uso de IB y encapsular la lógica de manejo de eventos dentro de un objeto designado (lo llaman comportamientos), eliminando así la lógica de su view / viewController haciéndolo más liviano.

No quiero UIButton ni nada de eso. Quiero hacer una subclase de UIControl directamente y hacer mi propio control, muy especial.

Pero por alguna razón, ninguno de los métodos que anulo se llama. La acción de acción objetivo funciona, y los objetivos reciben mensajes de acción apropiados. Sin embargo, dentro de mi subclase UIControl tengo que capturar las coordenadas táctiles, y la única manera de hacerlo parece ser anular a estos tipos:

- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event { NSLog(@"begin touch track"); return YES; } - (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event { NSLog(@"continue touch track"); return YES; }

Nunca se llaman, aunque el UIControl se crea una instancia con el inicializador designado de UIView, initWithFrame :.

Todos los ejemplos que puedo encontrar siempre usan UIButton o UISlider como base para la creación de subclases, pero quiero ir más cerca de UIControl ya que esa es la fuente de lo que quiero: coordenadas táctiles rápidas y no desafiladas.


Rápido

Estas son más de una forma de subclase UIControl . Cuando una vista principal necesita reaccionar para tocar eventos u obtener otros datos del control, esto generalmente se hace usando (1) objetivos o (2) el patrón delegado con eventos táctiles anulados. Para completar, también mostraré cómo (3) hacer lo mismo con un reconocedor de gestos. Cada uno de estos métodos se comportará como la siguiente animación:

Solo necesita elegir uno de los siguientes métodos.

Método 1: Agregue un objetivo

Una subclase UIControl tiene soporte para los objetivos ya integrados. Si no necesita pasar muchos datos al padre, este es probablemente el método que desea.

MyCustomControl.swift

import UIKit class MyCustomControl: UIControl { // You don''t need to do anything special in the control for targets to work. }

ViewController.swift

import UIKit class ViewController: UIViewController { @IBOutlet weak var myCustomControl: MyCustomControl! @IBOutlet weak var trackingBeganLabel: UILabel! @IBOutlet weak var trackingEndedLabel: UILabel! @IBOutlet weak var xLabel: UILabel! @IBOutlet weak var yLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() // Add the targets // Whenever the given even occurs, the action method will be called myCustomControl.addTarget(self, action: #selector(touchedDown), forControlEvents: UIControlEvents.TouchDown) myCustomControl.addTarget(self, action: #selector(didDragInsideControl(_:withEvent:)), forControlEvents: UIControlEvents.TouchDragInside) myCustomControl.addTarget(self, action: #selector(touchedUpInside), forControlEvents: UIControlEvents.TouchUpInside) } // MARK: - target action methods func touchedDown() { trackingBeganLabel.text = "Tracking began" } func touchedUpInside() { trackingEndedLabel.text = "Tracking ended" } func didDragInsideControl(control: MyCustomControl, withEvent event: UIEvent) { if let touch = event.touchesForView(control)?.first { let location = touch.locationInView(control) xLabel.text = "x: /(location.x)" yLabel.text = "y: /(location.y)" } } }

Notas

  • No hay nada de especial en los nombres de los métodos de acción. Podría haberles llamado cualquier cosa. Solo tengo que tener cuidado de deletrear el nombre del método exactamente como lo hice cuando agregué el objetivo. De lo contrario, sufrirá un bloqueo.
  • Los dos dos puntos en didDragInsideControl:withEvent: significa que se pasan dos parámetros al método didDragInsideControl . Si olvida agregar dos puntos o si no tiene la cantidad correcta de parámetros, obtendrá un bloqueo.
  • Gracias a esta respuesta por su ayuda con el evento TouchDragInside .

Pasando otros datos

Si tiene algún valor en su control personalizado

class MyCustomControl: UIControl { var someValue = "hello" }

a la que desea acceder en el método de acción de destino, puede pasar una referencia al control. Cuando establezca el objetivo, agregue dos puntos después del nombre del método de acción. Por ejemplo:

myCustomControl.addTarget(self, action: #selector(touchedDown), forControlEvents: UIControlEvents.TouchDown)

Tenga en cuenta que se touchedDown: (con dos puntos) y que no se touchedDown (sin dos puntos). Los dos puntos significan que se está pasando un parámetro al método de acción. En el método de acción, especifique que el parámetro es una referencia a su subclase UIControl . Con esa referencia, puede obtener datos de su control.

func touchedDown(control: MyCustomControl) { trackingBeganLabel.text = "Tracking began" // now you have access to the public properties and methods of your control print(control.someValue) }

Método 2: delegar el patrón y anular eventos táctiles

La subclase UIControl nos da acceso a los siguientes métodos:

  • beginTrackingWithTouch se beginTrackingWithTouch cuando el dedo toca por primera vez dentro de los límites del control.
  • continueTrackingWithTouch se invoca repetidamente a medida que el dedo se desliza por el control e incluso fuera de los límites del control.
  • se llama a endTrackingWithTouch cuando el dedo se levanta de la pantalla.

Si necesita un control especial de los eventos táctiles o si tiene mucha comunicación de datos para hacer con el padre, entonces este método puede funcionar mejor que agregar objetivos.

Aquí está cómo hacerlo:

MyCustomControl.swift

import UIKit // These are out self-defined rules for how we will communicate with other classes protocol ViewControllerCommunicationDelegate: class { func myTrackingBegan() func myTrackingContinuing(location: CGPoint) func myTrackingEnded() } class MyCustomControl: UIControl { // whichever class wants to be notified of the touch events must set the delegate to itself weak var delegate: ViewControllerCommunicationDelegate? override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool { // notify the delegate (i.e. the view controller) delegate?.myTrackingBegan() // returning true means that future events (like continueTrackingWithTouch and endTrackingWithTouch) will continue to be fired return true } override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool { // get the touch location in our custom control''s own coordinate system let point = touch.locationInView(self) // Update the delegate (i.e. the view controller) with the new coordinate point delegate?.myTrackingContinuing(point) // returning true means that future events will continue to be fired return true } override func endTrackingWithTouch(touch: UITouch?, withEvent event: UIEvent?) { // notify the delegate (i.e. the view controller) delegate?.myTrackingEnded() } }

ViewController.swift

Así es como el controlador de vista está configurado para ser el delegado y responder a eventos táctiles de nuestro control personalizado.

import UIKit class ViewController: UIViewController, ViewControllerCommunicationDelegate { @IBOutlet weak var myCustomControl: MyCustomControl! @IBOutlet weak var trackingBeganLabel: UILabel! @IBOutlet weak var trackingEndedLabel: UILabel! @IBOutlet weak var xLabel: UILabel! @IBOutlet weak var yLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() myCustomControl.delegate = self } func myTrackingBegan() { trackingBeganLabel.text = "Tracking began" } func myTrackingContinuing(location: CGPoint) { xLabel.text = "x: /(location.x)" yLabel.text = "y: /(location.y)" } func myTrackingEnded() { trackingEndedLabel.text = "Tracking ended" } }

Notas

  • Para obtener más información sobre el patrón de delegado, consulte esta respuesta .
  • No es necesario utilizar un delegado con estos métodos si solo se usan dentro del control personalizado. Podría haber agregado una declaración print para mostrar cómo se están convocando los eventos. En ese caso, el código se simplificaría a

    import UIKit class MyCustomControl: UIControl { override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool { print("Began tracking") return true } override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent?) -> Bool { let point = touch.locationInView(self) print("x: /(point.x), y: /(point.y)") return true } override func endTrackingWithTouch(touch: UITouch?, withEvent event: UIEvent?) { print("Ended tracking") } }

Método 3: use un reconocedor de gestos

Agregar un reconocedor de gestos se puede hacer en cualquier vista y también funciona en un UIControl . Para obtener resultados similares al ejemplo de la parte superior, usaremos un UIPanGestureRecognizer . Luego, al probar los distintos estados cuando se dispara un evento, podemos determinar qué está sucediendo.

MyCustomControl.swift

import UIKit class MyCustomControl: UIControl { // nothing special is required in the control to make it work }

ViewController.swift

import UIKit class ViewController: UIViewController { @IBOutlet weak var myCustomControl: MyCustomControl! @IBOutlet weak var trackingBeganLabel: UILabel! @IBOutlet weak var trackingEndedLabel: UILabel! @IBOutlet weak var xLabel: UILabel! @IBOutlet weak var yLabel: UILabel! override func viewDidLoad() { super.viewDidLoad() // add gesture recognizer let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(gestureRecognized(_:))) myCustomControl.addGestureRecognizer(gestureRecognizer) } // gesture recognizer action method func gestureRecognized(gesture: UIPanGestureRecognizer) { if gesture.state == UIGestureRecognizerState.Began { trackingBeganLabel.text = "Tracking began" } else if gesture.state == UIGestureRecognizerState.Changed { let location = gesture.locationInView(myCustomControl) xLabel.text = "x: /(location.x)" yLabel.text = "y: /(location.y)" } else if gesture.state == UIGestureRecognizerState.Ended { trackingEndedLabel.text = "Tracking ended" } } }

Notas

  • No olvide agregar los dos puntos después del nombre del método de action: "gestureRecognized:" en action: "gestureRecognized:" . El signo de dos puntos significa que se están transfiriendo los parámetros.
  • Si necesita obtener datos del control, puede implementar el patrón de delegado como en el método 2 anterior.

Creo que olvidó agregar [super] llamadas a touchesBegan / touchesEnded / touchesMoved. Métodos como

(BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)event

no están funcionando si sobreescribes toques Bean / toques Siguió así:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@"Touches Began"); } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@"Touches Moved"); } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { NSLog(@"Touches Ended"); }

¡Pero! Todo funciona bien si los métodos serán como:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesBegan:touches withEvent:event]; NSLog(@"Touches Began"); } - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesMoved:touches withEvent:event]; NSLog(@"Touches Moved"); } - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { [super touchesEnded:touches withEvent:event]; NSLog(@"Touches Ended"); }


El enfoque más simple que uso a menudo es extender UIControl, pero hacer uso del método heredado addTarget para recibir devoluciones de llamada para los diversos eventos. La clave es escuchar tanto al remitente como al evento para que pueda encontrar más información sobre el evento real (como la ubicación del lugar donde ocurrió el evento).

Entonces, simplemente la subclase UIControl y luego en el método init (asegúrese de que su initWithCoder también esté configurado si está usando nibs), agregue lo siguiente:

[self addTarget:self action:@selector(buttonPressed:forEvent:) forControlEvents:UIControlEventTouchUpInside];

Por supuesto, puede elegir cualquiera de los eventos de control estándar, incluido UIControlEventAllTouchEvents. Tenga en cuenta que al selector se le pasarán dos objetos. El primero es el control. El segundo es la información sobre el evento. Aquí hay un ejemplo de cómo usar el evento táctil para alternar un botón dependiendo de si el usuario presiona a la izquierda o a la derecha.

- (IBAction)buttonPressed:(id)sender forEvent:(UIEvent *)event { if (sender == self.someControl) { UITouch* touch = [[event allTouches] anyObject]; CGPoint p = [touch locationInView:self.someControl]; if (p.x < self. someControl.frame.size.width / 2.0) { // left side touch } else { // right side touch } } }

De acuerdo, esto es para controles bastante simples y puede llegar a un punto donde esto no le dé suficiente funcionalidad, pero esto ha funcionado para todos mis propósitos para controles personalizados hasta este punto y es realmente fácil de usar ya que normalmente me importa lo mismo controle los eventos que el UIControl ya admite (retoque, arrastre, etc.)

Ejemplo de código de control personalizado aquí: UISwitch personalizado (nota: esto no se registra con el botónPresionado: forEvent: selector, pero puede averiguarlo desde el código anterior)


Estaba teniendo problemas con un UIControl que no responde a beginTrackingWithTouch y continueTrackingWithTouch .

Descubrí que mi problema era cuando lo hice initWithFrame:CGRectMake() Hice el marco pequeño (el área que reacciona) y tenía solo un punto de punto donde funcionó. Hice el marco del mismo tamaño que el control y luego cada vez que presioné en cualquier lugar del control, respondió.


He buscado por mucho tiempo una solución a este problema y no creo que haya uno. Sin embargo, al inspeccionar más de cerca la documentación, creo que podría ser un malentendido el hecho de begintrackingWithTouch:withEvent: and continueTrackingWithTouch:withEvent: se supone que se llamará en absoluto ...

UIControl documentación de UIControl dice:

Es posible que desee extender una subclase de UIControl por dos motivos básicos:

Para observar o modificar el envío de mensajes de acción a destinos para eventos particulares Para hacer esto, anule sendAction:to:forEvent: evalúe el selector de datos pasados, el objeto de destino o la máscara de bits "Note" y proceda según sea necesario.

Para proporcionar un comportamiento de seguimiento personalizado (por ejemplo, para cambiar la apariencia resaltada) Para hacerlo, anule uno o todos los siguientes métodos: beginTrackingWithTouch:withEvent: continueTrackingWithTouch:withEvent: endTrackingWithTouch:withEvent:

La parte crítica de esto, que no es muy clara desde mi punto de vista, es que dice que es posible que desee extender una subclase de UIControl - NO es posible que desee extender UIControl directamente. Es posible que beginTrackingWithTouch:withEvent: and continuetrackingWithTouch:withEvent: no se suponga que se llame en respuesta a toques y que las subclases directas de UIControl supuestamente los llamen para que sus subclases puedan supervisar el seguimiento.

Así que mi solución a esto es anular touchesBegan:withEvent: y touchesMoved:withEvent: y llamarlos desde allí de la siguiente manera. Tenga en cuenta que el multitáctil no está habilitado para este control y que no me preocupan los toques terminados y los toques cancelados, pero si quiere ser completo / completo, probablemente debería implementarlos también.

- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { [super touchesBegan:touches withEvent:event]; // Get the only touch (multipleTouchEnabled is NO) UITouch* touch = [touches anyObject]; // Track the touch [self beginTrackingWithTouch:touch withEvent:event]; } - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event { [super touchesMoved:touches withEvent:event]; // Get the only touch (multipleTouchEnabled is NO) UITouch* touch = [touches anyObject]; // Track the touch [self continueTrackingWithTouch:touch withEvent:event]; }

Tenga en cuenta que también debe enviar cualquier UIControlEvent* mensajes que sean relevantes para su control utilizando sendActionsForControlEvents: - estos pueden ser llamados desde los super métodos, no lo he probado.


Sé que esta pregunta es antigua, pero tuve el mismo problema y pensé que debería dar mis 2 centavos.

Si su control tiene alguna sub-vista, beginTrackingWithTouch , touchesBegan , etc. podrían no ser llamados porque esas subvistas están tragándose los eventos táctiles.

Si no desea que esas subvistas manejen toques, puede establecer userInteractionEnabled en NO , por lo que las subvistas simplemente pasan el evento. A continuación, puede anular los touchesBegan/touchesEnded y administrar todos sus toques allí.

Espero que esto ayude.