0
votes

Swift UIButton: créez un bouton sur lequel vous devez appuyer trois fois

Ma question:

Est-il possible de sous-classer UIButton de telle manière que vous deviez appuyer trois fois sur le bouton avant qu'il n'appelle réellement la fonction?

< gagnantContexte

Je suis arbitre, donc pendant un match, je dois garder une trace du temps restant et des scores des deux équipes. Parce que j'étais fatigué d'utiliser à la fois du papier et un chronomètre, j'ai décidé de créer une application pour le gérer à ma place. Je suis programmeur après tout, alors pourquoi pas.

Comme il est impossible de garder mon téléphone entre mes mains tout le temps, je mets toujours mon téléphone (sur lequel l'application est exécutée) dans ma poche. Je ne le retire que lorsque j'ai besoin de changer le score ou lorsqu'il émet un bip (signalant que le temps est écoulé). Pour éviter que mon téléphone n'appuie accidentellement sur l'un des boutons alors qu'il est dans ma poche, je me suis assuré que vous deviez appuyer sur les boutons trois fois de suite pour être sûr que vous aviez vraiment l'intention d'appuyer dessus. Je l’ai fait en déclarant des variables indiquant le nombre de fois qu’elles ont été enfoncées dans la dernière seconde pour chaque bouton que j’ai à l’écran. Mais cela signifie aussi que je dois avoir autant de variables supplémentaires que le nombre de boutons à l'écran et lorsque les fonctions sont appelées, je dois d'abord vérifier combien de fois il a été appuyé avant pour déterminer s'il faut ou non exécuter le code. Cela fonctionne mais je me suis retrouvé avec un code vraiment compliqué. J'espérais que cela pourrait être mieux fait en sous-classant UIButton.


7 commentaires

Est-ce que cela répond à votre question? comment lancer une sous-classe UIButton?


@nvidot J'ai vu ce message. Je comprends comment fonctionne le sous-classement, donc je sais comment ajouter des variables et remplacer les initialiseurs. Ce que je ne sais pas, c’est comment faire en sorte que l’IBAction connectée à un bouton ne reçoive des appels qu’après avoir appuyé sur le bouton trois fois de suite.


Mettez un compteur dans l'action / la méthode.


@Magnas Eh bien, je pense que j'ai déjà fait ce que tu veux dire (voir contexte). Cela fonctionne, mais c'est vraiment compliqué.


Ne serait-il pas préférable d'avoir un écran de "verrouillage" orienté vers l'avant avec un bouton qui nécessite par exemple un geste de tapotement de deux secondes avant de déverrouiller le viewController pour exposer tous vos boutons de contrôle?


@Magnas je pourrais le faire mais j'ai choisi de le faire en utilisant un bouton que vous devez appuyer trois fois de suite parce que je dois être très rapide et quand j'ai besoin d'ajouter un point au score d'une équipe, je n'ai besoin que d'avoir accès à ce bouton. C'est plus efficace de cette façon.


«Mais cela signifie aussi que je dois avoir autant de variables supplémentaires que le nombre de boutons à l'écran et lorsque les fonctions sont appelées, je dois d'abord vérifier combien de fois il a été pressé avant pour déterminer s'il faut ou non exécuter le code. Cela fonctionne mais je me suis retrouvé avec un code vraiment compliqué. J'espérais que cela pourrait être mieux fait en sous-classant UIButton. » cela ne semble pas vraiment trop compliqué et l'OMI serait le meilleur moyen de le faire


3 Réponses :


2
votes

Si vous voulez éviter le code désordonné, une option consiste à sous-classer UIButton de UIKit et à implémenter le mécanisme de détection à trois pressions consécutives fourni par @vacawama.

Si vous voulez garder les choses simples, et aussi loin que il vous suffit de suivre le dernier bouton pour lequel 3 taps se produisent, alors vous n'avez besoin que d'un seul compteur associé au bouton pour lequel vous comptez et la dernière fois qu'il a été tapé.

Dès que le bouton change ou l'intervalle de temps est trop long, le bouton suivi change et le compteur revient à 1.

Lorsqu'une rangée de trois pressions sur le même bouton est détectée, vous déclenchez l'événement vers la cible d'origine et réinitialisez le compteur à 0.

import UIKit

class UIThreeTapButton : UIButton {

    private static var sender: UIThreeTapButton?
    private static var count = 0
    private static var lastTap = Date.distantPast

    private var action: Selector?
    private var target: Any?

    override func addTarget(_ target: Any?,
                            action: Selector,
                            for controlEvents: UIControl.Event) {
        if controlEvents == .touchUpInside {
            self.target = target
            self.action = action
            super.addTarget(self,
                            action: #selector(UIThreeTapButton.checkForThreeTaps),
                            for: controlEvents)
        } else {
            super.addTarget(target, action: action, for: controlEvents)
        }

    }

    @objc func checkForThreeTaps(_ sender: UIThreeTapButton, forEvent event: UIEvent) {
        let now = Date()
        if UIThreeTabButton.sender == sender &&
            now.timeIntervalSince(UIThreeTapButton.lastTap) < 0.5 {
            UIThreeTapButton.count += 1
            if UIThreeTapButton.count == 3 {
                _ = (target as? NSObject)?.perform(action, with: sender, with: event)
                UIThreeTapButton.count = 0
                UIThreeTapButton.lastTap = .distantPast
            }
        } else {
            UIThreeTapButton.sender = sender
            UIThreeTapButton.lastTap = now
            UIThreeTapButton.count = 1
        }
    }
}


0 commentaires

0
votes

J'essaierais simplement ce qui suit, sans même sous-classer. J'utiliserais le tag de chaque bouton pour multiplexer 2 informations: nombre de répétitions de taps, temps depuis le dernier tap.

La valeur multiplexée peut être 1000_000 * nbTaps + temps écoulé (mesuré en dixièmes de secondes) depuis le début de la lecture (cela fonctionnera pendant plus d'une journée complète).

Donc, un deuxième tap 10 minutes après le début de la lecture définira le tag sur 2000_000 + 6000 = 2006_000 Lors de la création de boutons (au début de la lecture, dans viewDidLoad), définissez leur balise sur 0.

Dans IBAction,

  • démultiplexer la balise pour trouver nbTaps et timeOfLastTap
  • calculer la nouvelle valeur de timeElapsed (en dixièmes de secondes)
  • si la différence avec timeOfLastTap est supérieure à 10 (1 seconde), réinitialiser la balise à 1000_000 + timeElapsed (donc nbTaps vaut 1) et renvoyer
  • si la différence est inférieure à 10, testez nbTaps (tel que stocké dans la balise)
  • s'il est inférieur à 2, réinitialisez simplement la balise à (nbTaps + 1) * 1000_000 + timeElapsed et retournez
  • if nbTaps> = 2, vous y êtes.
  • réinitialiser la balise à timeElapsed (en dixièmes de secondes) (nbTaps vaut désormais zéro)
  • effectuez l'IBAction dont vous avez besoin.

0 commentaires

1
votes

C'est délicat, mais faisable. Voici une implémentation de ThreeTapButton qui est une sous-classe de UIButton . Il fournit un traitement spécial pour l'événement .touchUpInside en le passant à un objet ThreeTapHelper . Tous les autres événements sont transmis à la superclasse UIButton .

class ViewController: UIViewController {

    @IBAction func tapMe3(_ sender: ThreeTapButton) {
        print("tapped 3 times")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

}

Pour utiliser ceci, soit:

  1. Utilisez ThreeTapButton à la place de UIButton dans votre code pour les boutons créés par programme.

  1. Remplacez la classe par ThreeTapButton dans Inspecteur d'identité pour les boutons du Storyboard.

Voici un exemple de bouton Storyboard:

// This is a helper object used by ThreeTapButton
class ThreeTapHelper {
    private var target: Any?
    private var action: Selector?
    private var count = 0
    private var lastTap = Date.distantPast


    init(target: Any?, action: Selector?) {
        self.target = target
        self.action = action
    }

    @objc func checkForThreeTaps(_ sender: ThreeTapButton, forEvent event: UIEvent) {
        let now = Date()
        if now.timeIntervalSince(lastTap) < 0.5 {
            count += 1
            if count == 3 {
                // Three taps in a short time have been detected.
                // Call the original Selector, forward the original
                // sender and event.
                _ = (target as? NSObject)?.perform(action, with: sender, with: event)
    
                count = 0
                lastTap = .distantPast
            }
        } else {
            lastTap = now
            count = 1
        }
    }
}
    

class ThreeTapButton: UIButton {
    private var helpers = [ThreeTapHelper]()
    
    // Intercept `.touchUpInside` when it is added to the button, and
    // have it call the helper instead of the user's provided Selector
    override func addTarget(_ target: Any?, action: Selector, for controlEvents: UIControl.Event) {
        if controlEvents == .touchUpInside {
            let helper = ThreeTapHelper(target: target, action: action)
            helpers.append(helper)
            super.addTarget(helper, action: #selector(ThreeTapHelper.checkForThreeTaps), for: controlEvents)
        } else {
            super.addTarget(target, action: action, for: controlEvents)
        }
    }
}

Dans ce cas, le @IBAction était connecté avec l'événement .touchUpInside .


Limitations:

Ceci est un premier essai de base à ce problème. Il présente les limitations suivantes:

  • Cela ne fonctionne qu'avec .touchUpInside . Vous pouvez facilement changer cela pour un autre événement si vous préférez.
  • Il est codé en dur pour rechercher 3 taps. Cela pourrait être rendu plus général.


4 commentaires

le contrôleur peut définir ThreeTagHelper comme gestionnaire d'action «normal» du bouton. Le repos ne changerait pas mais cela fonctionnerait pour tous les contrôles sans aucune sous-classe, non? mais à part ça, j'aime ça aussi


pour éviter un ivar, l'objet pourrait utiliser le stockage associatif;) nous n'avons même jamais besoin du pointeur, donc ce serait uniquement pour le maintenir en vie


@ Daij-Djan, je pense que nous avons besoin de la sous-classe pour que cela fonctionne pour les boutons Storyboard, non? Pouvez-vous me dire ce que vous entendez par stockage associatif? Quel avantage aurait-il en plus d'éliminer l'ivar?


oh ouais pour les storyboards .. Je n'ai pas utilisé ceux-ci depuis un moment: D a du sens. :)