【问题标题】:Press-and-hold button for "repeat fire"按住“重复射击”按钮
【发布时间】:2016-03-18 02:18:33
【问题描述】:

我已经提到了无数其他关于按住按钮的问题,但与 Swift 相关的问题并不多。我有一个使用 touchUpInside 事件连接到按钮的功能:

@IBAction func singleFire(sender: AnyObject){
    //code
}

...还有另一个函数,在按住同一个按钮时重复调用上述函数,并在不再按下按钮时停止:

@IBAction func speedFire(sender: AnyObject){

    button.addTarget(self, action: "buttonDown:", forControlEvents: .TouchDown)
    button.addTarget(self, action: "buttonUp:", forControlEvents: .TouchUpOutside)

    func buttonDown(sender: AnyObject){
        timer = NSTimer.scheduledTimerWithTimeInterval(0.3, target: self, selector: "singleFire", userInfo: nil, repeats: true)     
    }

    func buttonUp(sender: AnyObject){
        timer.invalidate()
    }
}

我不确定自己做错了什么,也不知道如何为同一个按钮设置触摸事件以实现不同的功能。

【问题讨论】:

    标签: ios swift nstimer addtarget


    【解决方案1】:

    在我最初的答案中,我回答了如何让按钮同时识别点击和长按的问题。在澄清的问题中,只要用户按住手指,您似乎希望此按钮持续“触发”。如果是这种情况,只需要一个手势识别器。

    例如,在 Interface Builder 中,将对象库中的长按手势识别器拖到按钮上,然后将“Min Duration”设置为零:

    然后您可以控制-从长按手势识别器拖动到助手编辑器中的代码并添加@IBAction来处理长按:

    weak var timer: Timer?
    
    @IBAction func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
        if gesture.state == .began {
            timer?.invalidate()
            timer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { [weak self] timer in
                guard let self = self else {
                    timer.invalidate()
                    return
                }
                self.handleTimer(timer)
            }
        } else if gesture.state == .ended || gesture.state == .cancelled {
            timer?.invalidate()
        }
    }
    
    func handleTimer(_ timer: Timer) {
        print("bang")
    }
    

    或者,如果您还想在用户将手指从按钮上移开时停止触发,请添加对手势位置的检查:

    @IBAction func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
        if gesture.state == .began {
            timer?.invalidate()
            timer = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: true) { [weak self] timer in
                guard let self = self else {
                    timer.invalidate()
                    return
                }
                self.handleTimer(timer)
            }
        } else if gesture.state == .ended || gesture.state == .cancelled || (gesture.state == .changed && !gesture.view!.bounds.contains(gesture.location(in: gesture.view))) {
            timer?.invalidate()
        }
    }
    

    下面是我的原始答案,即如何识别按钮上的点击和长按的不同问题:


    就个人而言,我会使用点击和长按手势识别器,例如:

    override func viewDidLoad() {
        super.viewDidLoad()
    
        let longPress = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
        button.addGestureRecognizer(longPress)
    
        let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        tap.shouldRequireFailure(of: longPress)
        button.addGestureRecognizer(tap)
    }
    
    @objc func handleTap(_ gesture: UITapGestureRecognizer) {
        print("tap")
    }
    
    @objc func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
        if gesture.state == .Began {
            print("long press")
        }
    }
    

    如果您愿意,也可以通过长按手势对.Ended 执行您的操作。这仅取决于所需的用户体验。

    仅供参考,您也可以直接在 Interface Builder 中添加这两个手势识别器(只需将相应的手势从对象库拖到按钮上,然后从手势识别器中 control-drag到@IBAction 函数),但通过以编程方式显示它更容易说明正在发生的事情。

    【讨论】:

    • 感谢您的回复。这似乎是一个优雅的解决方案,尽管 speedFire() 中的 timer = NSTimer.scheduledTimerWithTimeInterval(0.3, target: self, selector: "singleFire", userInfo: nil, repeats: true) 会由于某种原因导致它崩溃。
    • @Tim - 如果你要走那条路,你需要"singleFire:"(带有冒号,表示该方法有一个参数)。此外,您可能应该将其定义为func singleFire(timer: NSTimer) { ... },因为该参数将是计时器,而不仅仅是“发送者”,因此您不妨明确这一点。请注意,我也没有将其设为 @IBAction,因为这仅适用于您将通过 Interface Builder 连接到操作的方法,而此计时器处理程序并非如此。但是手势识别器的优点是不需要计时器。
    • 干杯罗布,这澄清了很多。我也试试看。
    • 感谢您的编辑,这个和另一个答案对我有很大帮助 - 我希望我能同时勾选它们。
    • 你好。不用担心。
    【解决方案2】:

    您希望在按住按钮时快速重复射击。

    您的buttonDownbuttonUp 方法需要在顶层定义,而不是在另一个函数内部。出于演示目的,更清楚的是放弃从情节提要中连接@IBActions,而只需在viewDidLoad 中设置按钮:

    class ViewController: UIViewController {
    
        @IBOutlet weak var button: UIButton!
        var timer: Timer?
        var speedAmmo = 20
    
        @objc func buttonDown(_ sender: UIButton) {
            singleFire()
            timer = Timer.scheduledTimer(timeInterval: 0.3, target: self, selector: #selector(rapidFire), userInfo: nil, repeats: true)
        }
    
        @objc func buttonUp(_ sender: UIButton) {
            timer?.invalidate()
        }
    
        func singleFire() {
            print("bang!")
        }
    
        @objc func rapidFire() {
            if speedAmmo > 0 {
                speedAmmo -= 1
                print("bang!")
            } else {
                print("out of speed ammo, dude!")
                timer?.invalidate()
            }
        }
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // These could be added in the Storyboard instead if you mark
            // buttonDown and buttonUp with @IBAction
            button.addTarget(self, action: #selector(buttonDown), for: .touchDown)
            button.addTarget(self, action: #selector(buttonUp), for: [.touchUpInside, .touchUpOutside])
        }
    }
    

    此外,我将.touchUpOutside 更改为[.touchUpInside, .touchUpOutside](以捕捉两个修饰事件)并在初始buttonDown 上调用singleFire 进行单次射击。通过这些更改,按下按钮会立即触发,然后只要按住按钮,就会每0.3 秒触发一次。


    可以在故事板中连接按钮,而不是在viewDidLoad 中设置它。在这种情况下,将@IBAction 添加到buttonDownbuttonUp。然后Control-单击情节提要中的按钮并从Touch Down旁边的圆圈拖动到func buttonDown,然后从Touch Up旁边的圆圈拖动内部修饰外部func buttonUp

    【讨论】:

    • 太棒了,非常感谢!只是想知道,似乎有必要将 singleFire 作为 @IBAction 函数移动到顶层的常规函数​​。这是因为 buttonDown() 和 IBAction singleFire 会同时发生吗?再次感谢
    • 传递给计时器的函数singleFire 的选择器表明它不带任何参数,因此将其作为@IBAction 并从计时器调用是没有意义的。我认为只有一个.TouchDown 处理函数并让它调用singleFire 会更清楚。
    • 快速提问:如果我有一个变量持有“速度弹药”,我将如何以每 0.3 间隔递减它?
    • 你可能想要[.TouchUpInside, .TouchDragExit]而不是[.TouchUpInside, .TouchUpOutside](即,如果用户将手指从开火按钮上拖开,你真的要继续开火吗?)。这仅取决于所需的用户体验。
    • .touchUpOutside 是我所缺少的(被卡在“开火”,或者在我的情况下驾驶),太棒了!
    【解决方案3】:

    我已将 @vacawama 示例代码更新为 swift 3。谢谢。

    @IBOutlet var button: UIButton!
    
    var timer: Timer!
    var speedAmmo = 100
    
    @IBAction func buttonDown(sender: AnyObject) {
        singleFire()
        timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector:#selector(rapidFire), userInfo: nil, repeats: true)
    }
    
    @IBAction func buttonUp(sender: AnyObject) {
        timer.invalidate()
    }
    
    func singleFire() {
        if speedAmmo > 0 {
            speedAmmo -= 1
            print("bang!")
        } else {
            print("out of speed ammo, dude!")
            timer.invalidate()
        }
    }
    
    func rapidFire() {
        if speedAmmo > 0 {
            speedAmmo -= 1
            print("bang!")
        } else {
            print("out of speed ammo, dude!")
            timer.invalidate()
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    
        button.addTarget(self, action:#selector(buttonDown(sender:)), for: .touchDown)
        button.addTarget(self, action:#selector(buttonUp(sender:)), for: [.touchUpInside, .touchUpOutside])
    }
    

    【讨论】:

      【解决方案4】:

      在提出自己的解决方案时,我采用了不同的方法。我创建了一个 UIButton 子类并将所有解决方案包含在其中。不同之处在于,我没有为处理程序使用 IBAction,而是在 UIButton 中创建了一个属性

      class RapidFireButton: UIButton {
      
          private var rapidFireTimer: Timer?
          override init(frame: CGRect) {
              super.init(frame: frame)
              commonInit()
          }
      
          required init?(coder aDecoder: NSCoder) {
              super.init(coder: aDecoder)
              commonInit()
          }
      
          init() {
              super.init(frame: .zero)
              commonInit()
          }
      
          private func commonInit() {
              addTarget(self, action: #selector(touchDownHandler), for: .touchDown)
              addTarget(self, action: #selector(touchUpHandler), for: .touchUpOutside)
              addTarget(self, action: #selector(touchUpHandler), for: .touchUpInside)
          }
      
          @objc private func touchDownHandler() {
              rapidFireTimer?.invalidate()
              rapidFireTimer = nil
              tapHandler(self)
              rapidFireTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { [unowned self] (timer) in
                  self.tapHandler(self)
              })
          }
      
          @objc private func touchUpHandler() {
              rapidFireTimer?.invalidate()
              rapidFireTimer = nil
          }
      
          var tapHandler: (RapidFireButton) -> Void = { button in
      
          } 
      }
      

      用法基本上是为按钮创建一个出口并像这样实现处理程序

      rapidFireButton.tapHandler = { button in
          //do stuff
      }
      

      【讨论】:

        【解决方案5】:

        你可以使用定时器

        在 github 上关注这个例子

        https://github.com/sadeghgoo/RunCodeWhenUIbuttonIsHeld

        【讨论】:

          【解决方案6】:

          Swift 5+

          根据 rob 的回答,现在有一种更好的方法可以做到这一点。

          添加长按手势识别器,方法是将其拖到情节提要中的按钮顶部,然后...

          然后您可以从长按手势识别器控制并拖动到 您在助手编辑器中的代码并添加一个@IBAction 来处理 长按: - 引用 Rob 的回答

          不同之处在于下面列出的代码:

          var timer: Timer?
          
          @IBAction func downButtonLongPressHandler(_ sender: UILongPressGestureRecognizer) {
                  if sender.state == .began {
                      timer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true, block: {_ in
                          self.downButtonPressedLogic()
                          self.doCalculations()
                      })
                  } else if sender.state == .ended || sender.state == .cancelled {
                      print("FINISHED UP LONG PRESS")
                      timer?.invalidate()
                      timer = nil
                  }
          }
          

          不再需要使用 NSTimer,你现在可以使用 Timer,你可以把定时器的代码放在更紧凑和更紧凑的块中不需要选择器。

          在我的例子中,我有另一个函数来处理按下 downButton 时的操作逻辑,但您可以将要处理的代码放在那里。

          您可以通过更改 withTimeInterval 参数值来控制连发速度

          您可以通过在情节提要中找到 longPressGesture 并更改它的 Min Duration 值来更改计时器开始的时间。我通常将我的设置为 0.5,这样你的正常按钮按下操作仍然可以工作(除非你不关心)。 如果您将其设置为 0,这将始终覆盖您的正常按钮按下操作。

          【讨论】:

            猜你喜欢
            • 2023-03-13
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2018-02-04
            • 2021-07-31
            • 2021-11-08
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多