Swift UIButton:制作一个需要按三次的按钮

Swift UIButton: Make a button that you need to press three times

我的问题:

是否可以将 UIButton 子类化为在实际调用函数之前必须按三下按钮?

上下文

我是一名裁判,所以在比赛中我需要跟踪剩余时间和两队的比分。因为我厌倦了同时使用纸张和秒表,所以我决定制作一个应用程序来帮我处理。毕竟我是个程序员,何乐而不为呢

因为一直把 phone 拿在手里是不切实际的,所以我总是把 phone(应用 运行 放在口袋里)放在口袋里。我只在需要更改乐谱或发出哔哔声(表示时间到了)时才将其取出。为了防止我的 phone 在我口袋里时不小心按下其中一个按钮,我确保你必须连续按下按钮三次,以确保你真的打算按下它。我通过声明变量来跟踪屏幕上每个按钮在最后一秒被按下的次数。但这也意味着我必须拥有与屏幕上按钮数量一样多的额外变量,并且在调用函数时我首先必须检查之前按下了多少次以确定是否执行代码。它有效,但我最终得到了一些非常混乱的代码。我希望通过子类化 UIButton 可以做得更好。

如果你想避免乱七八糟的代码,一种选择是从 UIKit 继承 UIButton 并实现@vacawama 提供的连续三击检测机制。

如果您想保持简单,并且只要您只需要跟踪发生 3 次点击的最后一个按钮,那么您只需要一个与您正在计数的按钮和最后一次相关联的计数器它被窃听了。

一旦按钮发生变化或时间间隔太长,跟踪的按钮发生变化,计数器回到1。

当检测到同一按钮连续三次点击时,您将向原始目标触发事件并将计数器重置为 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
        }
    }
}

我会简单地尝试以下方法,甚至不进行子类化。 我会使用每个按钮的标签来复用 2 个信息:点击的重复次数,自上次点击以来的时间。

多路复用值可以是 1000_000 * nbTaps + 自播放开始以来经过的时间(以十分之一秒为单位)(这将持续一整天以上)。

因此,在播放开始 10 分钟后第二次点击会将标签设置为 2000_000 + 6000 = 2006_000 创建按钮时(在播放开始时,在 viewDidLoad 中),将它们的标签设置为 0。

在IBAction中,

  • 多路分解标签以找到 nbTaps 和 timeOfLastTap
  • 计算 timeElapsed 的新值(以十分之一秒为单位)
  • 如果与 timeOfLastTap 的差异大于 10(1 秒),则将标签重置为 1000_000 + timeElapsed(因此 nbTaps 为 1)和 return
  • 如果差异小于 10,测试 nbTaps(存储在标签中)
  • 如果小于 2,则只需将标签重置为 (nbTaps + 1) * 1000_000 + timeElapsed 和 return
  • 如果 nbTaps >= 2,给你。
  • 将标签重置为 timeElapsed(以十分之一秒为单位)(nbTaps 现在为零)
  • 执行您需要的 IBAction。

这很棘手,但可行。这是 ThreeTapButton 的一个实现,它是 UIButton 的子 class。它通过将事件 .touchUpInside 传递给 ThreeTapHelper 对象来为事件提供特殊处理。所有其他事件都传递给 UIButton superclass.

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

要使用它,可以:

  1. 在代码中使用 ThreeTapButton 代替 UIButton 以编程方式创建按钮。

  1. 在 Storyboard 按钮的 Identity Inspector 中将 class 更改为 ThreeTapButton

下面是故事板按钮的示例:

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

}

在这种情况下,@IBAction.touchUpInside 事件相关联。


限制:

这是对这个问题的初步尝试。它具有以下限制:

  • 它只适用于 .touchUpInside。如果您愿意,可以轻松地将其更改为另一个事件。
  • 它被硬编码为寻找 3 个水龙头。这可以更通用。