I'm using a tap gesture recogniser so if the user taps on the screen the button fades out for 5 seconds and then if the user wants to see the buttons on the screen again they tap on the screen and the button fades in.
The Problem is:
I can't disable the button when it fades in so it won't automatically fade out again. I tried to invalidate the timer but that didn't work. To be more specific of what I want to do:
On app load, you see an enabled "Start Stop Button." - Tap anywhere on the screen and a 5 second timer starts to fade the button out and disables it. Once the button fades out and disables, I can tap anywhere on the screen to fade the button back in, enable it, and kill the timer so the button shows up as it was before I first tapped it.
class ViewController: UIViewController {
// Create these 3 properties in the top of your class
var secondToFadeOut = 5 // How many second do you want the view to idle before the button fades. You can change this to whatever you'd like.
var timer = Timer() // Create the timer!
var isTimerRunning: Bool = false // Need this to prevent multiple timers from running at the same time.
#IBOutlet weak var startStopButton: UIButton! // The outlet for your button. This is used to fade it in and out, and enable / disable it.
override func viewDidLoad() {
super.viewDidLoad()
startStopButton.isEnabled = true
runTimer()
// Add a tap gesture recognizer to the main view to determine when the screen was tapped (for the purpose of resetting the timer).
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tap(_:)))
self.view.addGestureRecognizer(tapRecognizer)
}
func runTimer() {
// Create the timer to run a method (in this case... updateTimer) every 1 second.
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(ViewController.updateTimer)), userInfo: nil, repeats: true)
// Set the isTimerRunning bool to true
isTimerRunning = true
}
#objc func updateTimer() {
// Every 1 second that this method runs, 1 second will be chopped off the secondToFadeOut property. If that hits 0 (< 1), then run the fadeOutButton and invalidate the timer so it stops running.
secondToFadeOut -= 1
print(secondToFadeOut)
if secondToFadeOut < 1 {
fadeOutButton()
timer.invalidate()
isTimerRunning = false
}
}
#objc func tap(_ gestureRecognizer: UITapGestureRecognizer) {
// When the view is tapped (based on the gesture recognizer), reset the secondToFadeOut property, fade in (and enable) the button.
//secondToFadeOut = 5
fadeInButton()
timer.invalidate()
//if isTimerRunning == false {
// runTimer()
//}
}
func fadeOutButton() {
// Fade out your button! I also disabled it here. But you can do whatever your little heart desires.
UIView.animate(withDuration: 0.5) {
self.startStopButton.alpha = 0.25
}
self.startStopButton.isEnabled = false
}
func fadeInButton() {
// Fade the button back in, and set it back to active (so it's tappable)
UIView.animate(withDuration: 0.5) {
self.startStopButton.alpha = 1
}
self.startStopButton.isEnabled = true
}
#IBAction func startStopButtonPressed(_ sender: UIButton) {
print("Start Stop Button Pressed")
}
}
My best guess is that you have a rogue Timer object that remains in memory even after you invalidate your current timer, and that is causing the button to fade out again after it fades in.
I have made several edits to your class. Check the code out:
class ViewController: UIViewController {
// Create these 3 properties in the top of your class
var secondToFadeOut = 5 // How many second do you want the view to idle before the button fades. You can change this to whatever you'd like.
var timer? = nil // Create the timer!
#IBOutlet weak var startStopButton: UIButton! // The outlet for your button. This is used to fade it in and out, and enable / disable it.
override func viewDidLoad() {
super.viewDidLoad()
startStopButton.isEnabled = true
runTimer()
// Add a tap gesture recognizer to the main view to determine when the screen was tapped (for the purpose of resetting the timer).
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tap(_:)))
self.view.addGestureRecognizer(tapRecognizer)
}
func runTimer() {
if timer == nil {
timer = Timer.scheduledTimer(timeInterval: secondToFadeOut, target: self, selector: (#selector(ViewController.timerFired)), userInfo: nil, repeats: false)
}
}
#objc func timerFired() {
timer = nil
if self.startStopButton.isEnabled {
fadeOutButton()
} else {
fadeInButton()
}
}
#objc func tap(_ gestureRecognizer: UITapGestureRecognizer) {
runTimer()
}
func fadeOutButton() {
UIView.animate(withDuration: 0.5) {
self.startStopButton.alpha = 0.25
}
self.startStopButton.isEnabled = false
}
func fadeInButton() {
UIView.animate(withDuration: 0.5) {
self.startStopButton.alpha = 1
}
self.startStopButton.isEnabled = true
}
#IBAction func startStopButtonPressed(_ sender: UIButton) {
print("Start Stop Button Pressed")
}
}
Related
I created a floating action button which i would like to dismiss all the action button when a user either taps the FAB when it's open or when the user taps anywhere on the screen and FAB is open. It is also important to note that the FAB is being displayed over a tableview and i want to retain the ability to select tableview cells.
In my implementation of the FAB i added a target to the FAB button which i use to open and close the FAB and i also implemented a tapGesture on the viewController with the tableview such that when a tap gesture is invoked i can close the FAB if open.
To make this work i did a bit of research and found out that i have to set
tap.cancelsTouchesInView = false
so that the tableView events continue working. However the side effect is that when i tap on the fab to close it two events are fired one from the tapGesture of the FAB and another from the button target which results in the FAB not closing when u tap on it if its open.
Is there a more elegant way of making the FAB be able to close when tapped whilst its open and also have the tap Gesture on the viewController close the fab when its open and a user taps anywhere on the screen.
Here is some of my code:
ViewController:
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self,
action: #selector(self.dismissActionButtons(_:)))
self.view.addGestureRecognizer(tap)
tap.cancelsTouchesInView = false
self.floatingActionButton.isHidden = true
}
#objc func dismissActionButtons(_ sender: UIButton) {
if !floatingActionButton.actionButtonsCarousel.isHidden {
self.floatingActionButton.animateActionButtonsDisappering()
}
}
Custom FAB View :
override init(frame: CGRect) {
super.init(frame: .zero)
actionButtonsCarousel.isHidden = true
self.translatesAutoresizingMaskIntoConstraints = false
self.mainButton.addTarget(self,
action: #selector(ACTFAB.fabButtonAction(_:)),
for: .touchUpInside)
}
#objc func fabButtonAction(_ sender: UIButton) {
if self.actionButtonsCarousel.isHidden {
self.animateActionButtonsAppering()
} else {
self.animateActionButtonsDisappering()
}
}
func animateActionButtonsAppering() {
self.actionButtonsCarousel.alpha = 0.0
self.actionButtonsCarousel.isHidden = false
UIView.transition(with: self, duration: 0.5, options: .preferredFramesPerSecond60, animations: {
self.actionButtonsCarousel.alpha = 1.0
})
self.mainButton.setImage(UIImage(named: "fab-open-icon"), for: .normal)
}
func animateActionButtonsDisappering() {
self.actionButtonsCarousel.alpha = 1.0
self.actionButtonsCarousel.isHidden = true
UIView.transition(with: self, duration: 0.3, options: .transitionCrossDissolve, animations: {
self.actionButtonsCarousel.alpha = 0.0
})
self.mainButton.setImage(UIImage(named: "fab-closed-icon"), for: .normal)
}
Two valid scenarios:
1 FAB is open -> click FAB -> FAB closes
2 FAB is open -> click anywhere other than FAB -> FAB closes
Scenario number 1 fails with my current code.
If I understand your question correctly, the issue is that tapping the FAB causes both the button's action to be fired but also, as you are passing the event through to the underlying viewController, the gestureRecogniser to fire too.
I'm assuming the button action is the primary event, and that when this fires you need to stop the gestureRecogniser. A gestureRecogniser has a .location(in:) method which allows you to get the first tap location (for a tapGestureRecogniser) in terms of any view, and a UIView has a .point(inside: with:) method that checks whether a CGPoint (in terms of its own coordinate space) is inside it bounds. Therefore you should be able to do something like this (from memory and not compiled, so may need some tweaking but hopefully it should get you started):
#objc func dismissActionButtons(_ sender: UIButton) {
let tapPoint = sender.location(in: customFABview)
if customFABview.point(inside: tapPoint, with: nil) &&
!floatingActionButton.actionButtonsCarousel.isHidden {
self.floatingActionButton.animateActionButtonsDisappering()
}
}
Continuing with #flanker's answer i created a boolean in the fab to check if the event was coming from tapGesture then i added it to the check conditional statement as follows:
var isComingFromGestureEvent: Bool = false
#objc func fabButtonAction(_ sender: UIButton) {
if self.actionButtonsCarousel.isHidden && !isComingFromGestureEvent {
self.animateActionButtonsAppering()
} else {
self.animateActionButtonsDisappering()
}
}
In the ViewController i then just used flanker's answer to set the boolean state as follows :
var locationInView: CGPoint = CGPoint(x: 0, y: 0)
override func viewDidLoad() {
super.viewDidLoad()
let tap = UITapGestureRecognizer(target: self,
action: #selector(self.dismissActionButtons(_:)))
self.view.addGestureRecognizer(tap)
pointInView = tap.location(in: floatingActionButton)
tap.cancelsTouchesInView = false
self.floatingActionButton.isHidden = true
}
#objc func dismissActionButtons(_ sender: UIButton) {
let tapPoint = pointInView
if floatingActionButton.point(inside: tapPoint, with: nil) &&
!floatingActionButton.actionButtonsCarousel.isHidden {
self.floatingActionButton.isComingFromGestureEvent = true
self.floatingActionButton.animateActionButtonsDisappering()
} else {
self.floatingActionButton.isComingFromGestureEvent = false
}
}
I'm trying to design an interface where a tap fades text in, then fades it out, but dragging two fingers up on the screen makes it brighter, and dragging two fingers down make the screen dimmer. I've gotten the text to fade in and out, but I can't seem to get the brightness functionality to work. The app is building, but the two finger gesture does nothing at all.
I have tried inserting code found here.
I have tried several other methods found on Stack Overflow, such as those found here as well, but with no luck.
UPDATE: I made some changes based off of feedback in the comments from #rmaddy and #leo-dabus. There's still nothing at all happening when I pan in the simulator or on my iPhone. I'm not sure if I should be using "recognizer.state" instead of "sender.state". I'm sure I'm making lots of beginner mistakes. Do I even have the code for the pan gesture in the right place? Here's what I have now:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// Hide text on launch
self.text.alpha = 0
}
// Introducing: The text!
#IBOutlet weak var text: UITextView!
// Show text upon tap of gesture recognizer
#IBAction func tap(_ sender: UITapGestureRecognizer) {
// fade in
UIView.animate(withDuration: 0.5, animations: {
self.text.alpha = 1.0
}) { (finished) in
// fade out
UIView.animate(withDuration: 4.0, animations: {
self.text.alpha = 0.0
})
}
}
#IBAction func twoFingerPan(_ sender: UIPanGestureRecognizer) {
if sender.state == UIPanGestureRecognizer.State.changed || sender.state == UIPanGestureRecognizer.State.ended {
let velocity:CGPoint = sender.velocity(in: self.view)
if velocity.y > 0 {
UIScreen.main.brightness -= 0.03
}
else {
UIScreen.main.brightness += 0.03
}
}
}
}
I suggest creating a couple of gesture recognizers. Something like this:
fileprivate func setGestureRecognizers() {
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tap(_:)))
tapGesture.delegate = self as? UIGestureRecognizerDelegate
tapGesture.numberOfTapsRequired = 1
view.addGestureRecognizer(tapGesture)
let panGesture = UIPanGestureRecognizer.init(target: self, action: #selector(twoFingerPan(_:)))
panGesture.delegate = self as? UIGestureRecognizerDelegate
panGesture.minimumNumberOfTouches = 1
view.addGestureRecognizer(panGesture)
}
On viewDidLoad call this method:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// Hide text on launch
self.text.alpha = 0
setGestureRecognizers()
}
Then I change a little your methods because I set the gesture recognizers using code and not storyboards, so I got rid of #IBAction and added #objc. Also I was testing this on my phone and comparing velocity.y > 0 made the gesture too sensitive to finger movement, so I changed it to 0.4 instead.
// Show text upon tap of gesture recognizer
#objc func tap(_ sender: UITapGestureRecognizer) {
// fade in
UIView.animate(withDuration: 0.5, animations: {
self.text.alpha = 1.0
}) { (finished) in
// fade out
UIView.animate(withDuration: 4.0, animations: {
self.text.alpha = 0.0
})
}
}
#objc func twoFingerPan(_ sender: UIPanGestureRecognizer) {
if sender.state == UIPanGestureRecognizer.State.changed || sender.state == UIPanGestureRecognizer.State.ended {
let velocity:CGPoint = sender.velocity(in: self.view)
if velocity.y > 0.4 {
UIScreen.main.brightness -= CGFloat(0.03)
}
else {
UIScreen.main.brightness += CGFloat(0.03)
}
}
}
Problem:
I have a UIButton that I'd like to fade out for a few seconds when the user doesn't touch the screen and fade them in when the user touches the screen. I think I might need to use a timer and some animation in the viewdidload part
#IBOutlet var startStopButton: UIButton!
#IBAction func startStopButtonTapped(_ sender: UIButton) {
}
override func viewDidLoad() {
super.viewDidLoad() }
I can help you here. Let me preface this answer with a usability concern. I'm not sure it makes sense to tap anywhere on the screen to reset (fade in) the button after X seconds (will the user know to do that?). You might want to add a button or some kind of text indicator to "tap here to reset the button(s)". This all ultimately depends on your application, but just throwing that out there :)
On with the answer!
The general idea here is you want to have a timer that runs a method every 1 second. If the timer hits zero, then you fade out and disable the startStop button. If the user taps anywhere on the screen (via gesture recognizer on the whole view), then fade the button in and enable it.
Now for the code part!
class ViewController: UIViewController {
// Create these 3 properties in the top of your class
var secondToFadeOut = 5 // How many second do you want the view to idle before the button fades. You can change this to whatever you'd like.
var timer = Timer() // Create the timer!
var isTimerRunning: Bool = false // Need this to prevent multiple timers from running at the same time.
#IBOutlet weak var startStopButton: UIButton! // The outlet for your button. This is used to fade it in and out, and enable / disable it.
override func viewDidLoad() {
super.viewDidLoad()
startStopButton.isEnabled = true
runTimer()
// Add a tap gesture recognizer to the main view to determine when the screen was tapped (for the purpose of resetting the timer).
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tap(_:)))
self.view.addGestureRecognizer(tapRecognizer)
}
func runTimer() {
// Create the timer to run a method (in this case... updateTimer) every 1 second.
timer = Timer.scheduledTimer(timeInterval: 1, target: self, selector: (#selector(ViewController.updateTimer)), userInfo: nil, repeats: true)
// Set the isTimerRunning bool to true
isTimerRunning = true
}
#objc func updateTimer() {
// Every 1 second that this method runs, 1 second will be chopped off the secondToFadeOut property. If that hits 0 (< 1), then run the fadeOutButton and invalidate the timer so it stops running.
secondToFadeOut -= 1
print(secondToFadeOut)
if secondToFadeOut < 1 {
fadeOutButton()
timer.invalidate()
isTimerRunning = false
}
}
#objc func tap(_ gestureRecognizer: UITapGestureRecognizer) {
// When the view is tapped (based on the gesture recognizer), reset the secondToFadeOut property, fade in (and enable) the button.
//secondToFadeOut = 5
fadeInButton()
timer.invalidate()
//if isTimerRunning == false {
// runTimer()
//}
}
func fadeOutButton() {
// Fade out your button! I also disabled it here. But you can do whatever your little heart desires.
UIView.animate(withDuration: 0.5) {
self.startStopButton.alpha = 0.25
}
self.startStopButton.isEnabled = false
}
func fadeInButton() {
// Fade the button back in, and set it back to active (so it's tappable)
UIView.animate(withDuration: 0.5) {
self.startStopButton.alpha = 1
}
self.startStopButton.isEnabled = true
}
#IBAction func startStopButtonPressed(_ sender: UIButton) {
print("Start Stop Button Pressed")
}
}
Hope that makes sense, but do let me know if you have any more questions!
looking for some help for this, I have a timer that works after submitting a password which is great, but I then need to disable the button after the timer starts and is disabled for a period of time, (in the code I have entered a nominal 90 seconds)
however the button is not disabling.
if anybody could show me where I am going wrong that would be awesome.
import UIKit
class appiHour: UIViewController {
var timer = Timer()
var counter = 60
var password_Text: UITextField?
func enableButton() {
self.timerStartButton.isEnabled = true
}
#IBOutlet weak var timerLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func timerStartButton(_ sender: Any) {
var password_Text: UITextField?
let alertController = UIAlertController(title: "To start your own 2 Cocktails for £10 APPi Hour", message: "get a memeber of the team to enter the password, but use it wisely, as you can only use it once per day, with remember great power comes great responsability", preferredStyle: UIAlertControllerStyle.alert)
let tickoff_action = UIAlertAction(title: "let the APPiness commence", style: UIAlertActionStyle.default) {
action -> Void in
self.timerStartButton.isEnabled = false
Timer.scheduledTimer(timeInterval: 90, target: self, selector: #selector(appiHour.enableButton), userInfo: nil, repeats: false)
if let password = password_Text?.text{
print("password = \(password)")
if password == "baruba151" {
self.counter = 60
self.timerLabel.text = String(self.counter)
self.timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(appiHour.updateCounter), userInfo: nil, repeats: true)
}
} else {
print("No password entered")
}
}
alertController.addTextField { (txtpassword) -> Void in
password_Text = txtpassword
password_Text!.isSecureTextEntry = true
password_Text!.placeholder = ""
}
alertController.addAction(tickoff_action)
self.present(alertController, animated: true, completion: nil)
}
#IBOutlet weak var timerStartButton: UIButton!
func updateCounter() {
counter -= 1
timerLabel.text = String(counter)
if counter == 0{
timer.invalidate()
counter = 0
}
}
}
As a secondary question is it possible to run the timer while the app is in the background? i know apple frowns on this aside for Sat Nav, Music apps etc. But is there a method in which the timer is held and a notification is sent locally letting the user know the timer has ended?
thanks in advance.
I suspect that your action may not be hooked up to your button. I just tried the following code with no issues. The button gets disabled, and then enabled 5 seconds later:
class ViewController: UIViewController {
#IBOutlet weak var myButton: UIButton!
#IBAction func ButtonPressed(_ sender: Any) {
myButton.isEnabled = false
Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(myTimerTick), userInfo: nil, repeats: false)
}
func myTimerTick() {
myButton.isEnabled = true
}
}
So make sure your outlets and actions are hooked up to the button correctly. If you right click on your button, you should see the dots filled in next to the outlet and action. You should see similarly filled in dots in your code.
You can further verify it is hooked up by placing a breakpoint in your "timerStartButton" method and making sure that breakpoint is hit.
Edit to further clarify: You need to connect your code to your Interface build objects. See this article from Apple for a complete tutorial on how to do that.
I'm not 100% sure if this is what you mean. But this would at least satisfy the first part of your request: disable a button whilst a timer is running, and re-enable it once the timer stops.
#IBOutlet weak var myButton: UIButton!
#IBOutlet weak var timerCount: UILabel!
#IBAction func buttonPressed(_ sender: UIButton) {
var count = 0
sender.isEnabled = false
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [unowned self] timer in
count += 1
if count == 5 {
count = 0
sender.isEnabled = true
timer.invalidate()
}
self.timerCount.text = "\(count)"
}
}
Here's a couple of screenshots of what you get.
It's enabled when the user starts off, disabled whilst the count is going then reverts back to its original state with counter at 0 and button enabled. Is that what you're going for?
As far as your second question, what do you mean by
the timer is held
Do you want the timer to keep running whilst the app is in the background, then update the user once the timer has elapsed? If so, take a look at this answer which should point you in the right direction: Continue countdown timer when app is running in background/suspended
I have referred to countless other questions about a press-and-hold button but there aren't many related to Swift. I have one function connected to a button using the touchUpInside event:
#IBAction func singleFire(sender: AnyObject){
//code
}
...and another function that is meant to call the function above repeatedly while the same button is held down, and stop when the button is no longer pressed:
#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()
}
}
I'm not sure what I'm doing wrong, and I don't know how to setup touch events to the same button for a different function.
You want rapid repeat fire when your button is held down.
Your buttonDown and buttonUp methods need to be defined at the top level, and not inside of another function. For demonstration purposes, it is clearer to forgo wiring up #IBActions from the Storyboard and just set up the button in 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])
}
}
Also, I changed .touchUpOutside to [.touchUpInside, .touchUpOutside] (to catch both touch up events) and call singleFire on the initial buttonDown for single fire. With these changes, pressing the button fires immediately, and then fires every 0.3 seconds for as long as the button is held down.
The button can be wired up in the Storyboard instead of setting it up in viewDidLoad. In this case, add #IBAction to buttonDown and buttonUp. Then Control-click on your button in the Storyboard and drag from the circle next to Touch Down to func buttonDown, and drag from the circles next to Touch Up Inside and Touch Up Outside to func buttonUp.
In my original answer, I answered the question of how to have a button recognize both a tap and a long press. In the clarified question, it appears you want this button to continuously "fire" as long as the user holds their finger down. If that's the case, only one gesture recognizer is needed.
For example, in Interface Builder, drag a long press gesture recognizer from the object library onto the button and then set the "Min Duration" to zero:
Then you can control-drag from the long press gesture recognizer to your code in the assistant editor and add an #IBAction to handle the long press:
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")
}
Or, if you also want to stop firing when the user drags their finger off of the button, add a check for the location of the gesture:
#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()
}
}
My original answer, answering the different question of how to recognize both taps and long presses on a button, is below:
Personally, I'd use tap and long press gesture recognizers, e.g.:
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")
}
}
If you want, with the long press gesture, you could perform your action upon .Ended, too. It just depends upon the desired UX.
FYI, you can also add these two gesture recognizers right in Interface Builder, too, (just drag the respective gestures from the object library on to the button and then control-drag from the gesture recognizer to #IBAction functions) but it was easier to illustrate what's going on by showing it programmatically.
I took a different approach when coming up with my own solution. I created a UIButton subclass and enclosed all the solution inside. The difference is that instead of using an IBAction for the handler I created a property in the 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
}
}
Usage is basically creating an outlet for the button and implementing the handler like so
rapidFireButton.tapHandler = { button in
//do stuff
}
I updated #vacawama example codes to swift 3. Thanks.
#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])
}
Swift 5+
Based on rob's answer there is a nicer way to do this now.
Add the long press gesture recognizer by dragging it on-top of the button in the storyboard and then ...
Then you can control-drag from the long press gesture recognizer to
your code in the assistant editor and add an #IBAction to handle the
long press:
- Quote from Rob's Answer
The difference is in the code which is listed below:
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
}
}
You no longer need to use NSTimer, you can just use Timer now and you can just put the code for the timer in the block which is much more compact and no need for selectors.
In my case I had another function that handles the logic for what to do when the downButton was pressed, but you can put your code you want to handle in there.
You can control the speed of the repeat fire by changing the withTimeInterval parameter value.
You can change the timing for the timer to start by finding the longPressGesture in your storyboard and changing it's Min Duration value. I usually set mine at 0.5 so that your normal button press actions can still work (unless you don't care about that). If you set it to 0 this will ALWAYS override your normal button press actions.
You can use Timer
follow this example on github
https://github.com/sadeghgoo/RunCodeWhenUIbuttonIsHeld
viewDidLoad
let longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.touchHoldView(sender:)))
longPressGesture.minimumPressDuration = 2
myUIView.addGestureRecognizer(longPressGesture)
#objc func touchHoldView(sender: UITapGestureRecognizer) {
if sender.state == .began {
//code..
}
}