web-dev-qa-db-fra.com

Comment sous-classer correctement UIControl?

Je ne veux pas UIButton ou quelque chose comme ça. Je veux directement sous-classe UIControl et créer mon propre contrôle très spécial.

Mais pour une raison quelconque, aucune des méthodes que je substitue n’est jamais appelée. Le travail cible-action fonctionne et les cibles reçoivent les messages d'action appropriés. Cependant, dans ma sous-classe UIControl, je dois capturer les coordonnées tactiles, et le seul moyen de le faire semble être de surcharger ces types:

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

Ils ne sont jamais appelés, même si la UIControl est instanciée avec l'initialiseur désigné par UIView, initWithFrame:.

Tous les exemples que je peux trouver utilisent toujours une UIButton ou UISlider comme base pour le sous-classement, mais je veux aller plus près de UIControl car c'est la source de ce que je veux: coordonnées tactiles rapides et non retardées.

64
HelloMoon

Je sais que cette question est ancienne, mais j'avais le même problème et je pensais que je devrais donner mes 2 cents.

Si votre contrôle comporte des sous-vues, il est possible que beginTrackingWithTouch, touchesBegan, etc. ne soit pas appelé car ces sous-vues avalent les événements tactiles.

Si vous ne voulez pas que ces sous-vues gèrent les contacts, vous pouvez définir userInteractionEnabled à NO, afin que les sous-vues transmettent simplement l'événement. Ensuite, vous pouvez remplacer touchesBegan/touchesEnded et gérer tous vos contacts.

J'espère que cela t'aides.

63
pixelfreak

Rapide

C’est plus d’un moyen de sous-classe UIControl. Lorsqu'une vue parent doit réagir à des événements tactiles ou obtenir d'autres données du contrôle, cela se fait généralement à l'aide de (1) cibles ou (2) du modèle de délégué avec des événements tactiles remplacés. Pour être complet, je montrerai également comment (3) faire la même chose avec un identificateur de geste. Chacune de ces méthodes se comportera comme l'animation suivante:

 enter image description here

Il vous suffit de choisir l'une des méthodes suivantes.


Méthode 1: Ajouter une cible

Une sous-classe UIControl prend en charge les cibles déjà intégrées. Si vous n'avez pas besoin de transmettre beaucoup de données au parent, cette méthode est probablement celle que vous souhaitez. 

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)"
        }
    }
}

Remarques

  • Les noms de méthode d'action n'ont rien de spécial. J'aurais pu les appeler n'importe quoi. Je dois juste faire attention à épeler le nom de la méthode exactement comme je l'ai fait où j'ai ajouté la cible. Sinon, vous obtenez un crash.
  • Les deux points dans didDragInsideControl:withEvent: signifient que deux paramètres sont passés à la méthode didDragInsideControl. Si vous oubliez d'ajouter deux points ou si vous n'avez pas le nombre correct de paramètres, vous obtiendrez un crash.
  • Merci à cette réponse pour l’aide concernant l’événement TouchDragInside

Transmettre d'autres données

Si vous avez une valeur dans votre contrôle personnalisé 

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

auquel vous souhaitez accéder dans la méthode d’action cible, vous pouvez alors transmettre une référence au contrôle. Lorsque vous définissez la cible, ajoutez deux points après le nom de la méthode d'action. Par exemple:

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

Notez qu'il s'agit de touchedDown: (avec deux points) et non de touchedDown (sans deux points). Les deux points signifient qu'un paramètre est transmis à la méthode d'action. Dans la méthode d'action, spécifiez que le paramètre est une référence à votre sous-classe UIControl. Avec cette référence, vous pouvez obtenir des données de votre contrôle.

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éthode 2: déléguer un modèle et remplacer des événements tactiles

Le sous-classement UIControl nous donne accès aux méthodes suivantes:

  • beginTrackingWithTouch est appelé lorsque le doigt touche pour la première fois dans les limites du contrôle.
  • continueTrackingWithTouch est appelé à plusieurs reprises lorsque le doigt glisse sur le contrôle et même en dehors des limites du contrôle.
  • endTrackingWithTouch est appelé lorsque le doigt s’éloigne de l’écran.

Si vous avez besoin d'un contrôle spécial des événements tactiles ou si vous avez beaucoup de communication de données à faire avec le parent, cette méthode peut alors être plus efficace que l'ajout de cibles.

Voici comment faire:

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

Voici comment le contrôleur de vue est configuré pour être le délégué et répondre aux événements tactiles de notre contrôle personnalisé.

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"
    }
}

Remarques

  • Pour en savoir plus sur le modèle de délégué, voir cette réponse .
  • Il n'est pas nécessaire d'utiliser un délégué avec ces méthodes si elles ne sont utilisées que dans le contrôle personnalisé lui-même. J'aurais pu simplement ajouter une déclaration print pour montrer comment les événements sont appelés. Dans ce cas, le code serait simplifié pour

    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éthode 3: utiliser un outil de reconnaissance de geste

L'ajout d'un identificateur de geste peut être fait sur n'importe quelle vue et cela fonctionne également sur une UIControl. Pour obtenir des résultats similaires à l'exemple du haut, nous allons utiliser un UIPanGestureRecognizer. Ensuite, en testant les différents états lorsqu'un événement est déclenché, nous pouvons déterminer ce qui se passe.

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"
        }
    }
}

Remarques

  • N'oubliez pas d'ajouter les deux points après le nom de la méthode d'action dans action: "gestureRecognized:". Les deux points signifient que les paramètres sont transmis.
  • Si vous devez extraire des données du contrôle, vous pouvez implémenter le modèle de délégué comme dans la méthode 2 ci-dessus.
22
Suragch

J'ai longtemps et longuement cherché une solution à ce problème et je ne pense pas qu'il en existe une. Cependant, en examinant de plus près la documentation, je pense que le fait que begintrackingWithTouch:withEvent: et continueTrackingWithTouch:withEvent: soient censés être appelés est peut-être un malentendu ...

UIControl documentation indique:

Vous souhaiterez peut-être prolonger une UIControl sous-classe pour deux raisons fondamentales:

Observer ou modifier l’envoi de messages d'action aux cibles pour événements particuliers Pour ce faire, remplacez sendAction:to:forEvent:, évaluez le sélecteur passé, objet cible ou Masque de bits «Remarque» et procédez comme suit Champs obligatoires.

Pour fournir un comportement de suivi personnalisé (par exemple, pour changer l'apparence en surbrillance ) Pour ce faire, remplacez un ou toutes les méthodes suivantes: beginTrackingWithTouch:withEvent:, continueTrackingWithTouch:withEvent:, endTrackingWithTouch:withEvent:.

La partie critique de ceci, ce qui n’est pas très clair à mon avis, est qu’il est dit que vous voudrez peut-être étendre une sous-classe UIControl - NON, vous voudrez peut-être étendre directement UIControl. Il est possible que beginTrackingWithTouch:withEvent: et continuetrackingWithTouch:withEvent: ne soient pas censés être appelés en réponse à des effleurements et que les sous-classes directes UIControl soient supposées les appeler afin que leurs - sous-classes puissent surveiller le suivi.

Donc, ma solution à cela est de remplacer touchesBegan:withEvent: et touchesMoved:withEvent: et de les appeler comme suit. Notez que le multi-touch n'est pas activé pour ce contrôle et que je ne me soucie pas des touches finies et des événements annulés, mais si vous voulez être complet/complet, vous devriez probablement également les implémenter.

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

Notez que vous devriez également envoyer tous les messages UIControlEvent* pertinents pour votre contrôle en utilisant sendActionsForControlEvents: - ceux-ci peuvent être appelés à partir des super méthodes, je ne l’ai pas testé.

20
jhabbott

Je pense que vous avez oublié d'ajouter [super] appels aux touchesBegan/touchesEnded/touchesMoved . Des méthodes telles que 

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

ne fonctionne pas si vous écrasez les touchesBegan/touchesEnded comme ceci:

- (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");
}

Mais! Tout fonctionne bien si les méthodes ressemblent à:

- (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");
}
9
tt.Kilew

L'approche la plus simple que j'utilise souvent consiste à étendre UIControl, mais à utiliser la méthode héritée addTarget pour recevoir des rappels pour les différents événements. L'essentiel est d'écouter à la fois l'expéditeur et l'événement afin de pouvoir obtenir plus d'informations sur l'événement réel (tel que l'emplacement de l'événement).

Donc, il suffit simplement de sous-classer UIControl puis dans la méthode init (assurez-vous que votre initWithCoder est également configuré si vous utilisez des nibs), ajoutez ce qui suit:

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

Bien entendu, vous pouvez choisir l'un des événements de contrôle standard, y compris UIControlEventAllTouchEvents. Notez que le sélecteur va passer deux objets. Le premier est le contrôle. La seconde est l’information sur l’événement. Voici un exemple d'utilisation de l'événement tactile pour basculer un bouton selon que l'utilisateur a appuyé à gauche et à droite.

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

Certes, il s’agit de contrôles assez simples et vous risquez de ne pas vous donner suffisamment de fonctionnalités, mais cela a fonctionné pour tous les besoins de contrôle personnalisé jusqu’à présent, et il est très facile à utiliser car je me soucie généralement de la même contrôler les événements déjà pris en charge par UIControl (retouches, glisser, etc.)

Échantillon de code du contrôle personnalisé ici: UISwitch personnalisé (remarque: cela ne s’enregistre pas avec le boutonPrimé: forEvent: sélecteur, mais vous pouvez le comprendre à partir du code ci-dessus)

8
Duane Homick

J'avais des problèmes avec un UIControl qui ne répondait pas à beginTrackingWithTouch et continueTrackingWithTouch.

J'ai trouvé que mon problème était quand je faisais initWithFrame:CGRectMake() j'ai fait le cadre trop petit (la zone qui réagit) et je n'avais qu'un point ou deux points où cela fonctionnait. J'ai fait le cadre de la même taille que le contrôle et puis chaque fois que j'ai appuyé n'importe où dans le contrôle, il a répondu.

3
digitalmasters

Obj C

J'ai trouvé un article lié de 2014 https://www.objc.io/issues/13-architecture/behaviors/ .

Ce qui est intéressant, c’est que son approche consiste à utiliser IB et à encapsuler la logique de gestion des événements dans un objet désigné (ils l’appellent comportements), supprimant ainsi la logique de votre view/viewController, ce qui la rend plus légère. 

0
supergegs