I want to detect number of taps
User can tap multiple times and I have to perform action based on number of taps.
I tried using UIButton with the below code but its detecting all the taps
if I tap three times, It prints
1
2
3
Code -
tapButton.addTarget(self, action: #selector(multipleTap(_:event:)), for: UIControl.Event.touchDownRepeat)
#objc func multipleTap(_ sender: UIButton, event: UIEvent) {
let touch: UITouch = event.allTouches!.first!
print(touch.tapCount)
}
I need the output to be just 3 if i tap three times.
Edit 1: for ex - if you tap three times in youtube it will forward 30 seconds and if you tap 4 times it will forward 40 secs.
You aren’t defining the problem clearly. Are you saying that you want to detect a group of repeated taps within a short time as a single, multi-tap event, and report the number of taps? If so, you need to add logic to do that. Decide how long to wait between taps before you consider the event complete.
You’ll then need to keep track of how many taps have occurred, and how long it’s been since the last tap. You’ll need a timer that fires when the inter-tap interval has passed with no new taps so that you can consider the event complete.
All this sounds a lot more like a tap gesture recognizer than a button. Tap gesture recoginizers are written to detect a specific number of taps, with timeouts and such, but don’t respond to variable numbers of taps. You might want to create a custom gesture recognizer that responds to a variable number of taps instead of a button.
introduce var to hold tap count
your modified code will be:
tapButton.addTarget(self, action: #selector(singleTap(_:)), for: .touchUpInside)
private var numberOfTaps = 0
private var lastTapDate: Date?
#objc private func singleTap(_ sender: UIButton) {
if let lastTapDate = lastTapDate, Date().timeIntervalSince(lastTapDate) <= 1 { // less then a second
numberOfTaps += 1
} else {
numberOfTaps = 0
}
lastTapDate = Date()
if numberOfTaps == 3 {
// do your rewind stuff here 30 sec
numberOfTaps = 0
}
}
edit: I'm not mind reader, but I guess you look for something like above (updated code)
class ViewController: UIViewController {
var tapCount: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
tapButton.addTarget(self, action: #selector(multipleTap(sender:)), for: .touchUpInside)
}
#objc func multipleTap(sender: UIButton) {
tapCount += 1
if tapCount == 3 {
print(tapCount) //3
}
}
}
I'm not sure for what exactly you need this, but below Duncan's answer I read that you need to copy something from YouTube logic.
Yes, of course you can use tap gesture recognizer, but if you need from some reason UIButton, you can create its subclass.
After first touch nothing happens, but then after every next touch valueChanged handler which you will set in view controller gets called. If you don't press button again to certain wait duration, touches will be reset to 0.
class TouchableButton: UIButton {
var waitDuration: Double = 1.5 // change this for custom duration to reset
var valueChanged: (() -> Void)? // set this to handle press of button
var minimumTouches: Int = 2 // set this to change number of minimum presses
override init(frame: CGRect) {
super.init(frame: frame)
setTarget()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setTarget()
}
private func setTarget() {
addTarget(self, action: #selector(buttonTouched), for: .touchUpInside)
}
#objc private func buttonTouched() {
touches += 1
}
private var timer: Timer?
private var touches: Int = 0 {
didSet {
if touches >= minimumTouches {
valueChanged?()
timer?.invalidate()
timer = Timer.scheduledTimer(withTimeInterval: waitDuration, repeats: false) { _ in
self.touches = 0
}
}
}
}
}
then when you need to set what should happen after every touch, you can set value changed handler
button.valueChanged = { // button of type `TouchableButton`
//print("touched")
... // move 10s forward or backwards
}
you can also change waitDuration property which specify wait time between last press and time when touches will be reset
button.waitDuration = 1
also you can set number of minimum touches (first time when valueChanged gets executed)
button.minimumTouches = 3
You need to check time difference for touch events.
For ex: If button is pressed 4 times and in particular time, say 1 sec, if it does not get pressed again then you can print 4 taps otherwise don't print.
For this you also need to call the timer so that you can check the time of last event and print accordingly.
var timer : Timer?
var timeDuration = 2.0
var tapCount = 0
var lastButtonPressedTime : Date?
#IBAction func tapCountButtonAction(_ sender: UIButton) {
lastButtonPressedTime = Date()
if(tapCount == 0){
tapCount = 1
timer = Timer.scheduledTimer(withTimeInterval: timeDuration, repeats: true, block: { (timer) in
let difference = Calendar.current.dateComponents([.second], from:self.lastButtonPressedTime!, to:Date())
if(difference.second! > 1){
print(self.tapCount)
timer.invalidate()
self.tapCount = 0
}
})
}else{
tapCount += 1
}
}
The technique used here is introducing a time delay in executing the
final action
static let tapCount = 0
tapButton.addTarget(self, action: #selector(multipleTap(_:event:)), for: UIControl.Event.touchDownRepeat)
// An average user can tap about 7 times in 1 second using 1 finger
DispatchQueue.main.asyncAfter(deadLine: .now() + 1.0 /* We're waiting for a second till executing the target method to observe the total no of taps */ ) {
self.performAction(for: tapCount)
}
#objc func multipleTap(_ sender: UIButton, event: UIEvent) {
tapCount += 1
}
/// Perform the respective action based on the taps
private func performAction(for count: Int) {
// Resetting the taps after 1 second
tapCount = 0
switch (count){
case 2:
// Handle 2 clicks
case 3:
// Handle 3 clicks
case 4:
// Handle 4 clicks
default:
print("Action undefined for count: \(count)")
}
}
Related
My question:
Is it possible to subclass UIButton in such a way that you have to press the button three times before it actually calls the function?
Context
I am a referee so during a match I need to keep track of the remaining time and both teams’ scores. Because I was tired of using both paper and a stopwatch I decided to make an app to handle it for me. I’m a programmer after all so why not.
Since it’s impractical to keep my phone in my hands the whole time I always put my phone (on which the app is running) in my pocket. I only take it out when I need to change the score or when it beeps (signalling the time’s up). To prevent my phone from accidentally pressing one of the buttons while it’s in my pocket I made sure you have to press the buttons three times in a row to make sure you really intended to press it. I did this by declaring variables keeping track of how many times it’s been pressed in the last second for each button I have on screen. But this also means I have to have as many variables extra as the amount of buttons on screen and when the functions are called I first have to check how many times it has been pressed before to determine whether or not to execute the code. It works but I ended up with some really messy code. I was hoping it could be done better by subclassing UIButton.
If you want to avoid the messy code, one option is to subclass UIButton from UIKit and implement the three taps in a row detection mechanism provided by #vacawama.
If you want to keep it simple, and as far as you only need to track the last button for which 3 taps occurs, then you only need one counter associated with the button for which you are counting and the last time it was tapped.
As soon as the button change or the time interval is too long, the tracked button change and the counter goes back to 1.
When a row of three taps on the same button is detected, you fire the event towards the original target and reset the counter to 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
}
}
}
This is tricky, but doable. Here is an implementation of ThreeTapButton that is a subclass of UIButton. It provides special handling for the event .touchUpInside by passing that to a ThreeTapHelper object. All other events are passed through to the 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)
}
}
}
To use this, either:
Use ThreeTapButton in place of UIButton in your code for programmatically created buttons.
or
Change the class to ThreeTapButton in the Identity Inspector for Storyboard buttons.
Here is an example of a Storyboard button:
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.
}
}
In this case, the #IBAction was connected with the .touchUpInside event.
Limitations:
This is a basic first stab at this problem. It has the following limitations:
It only works with .touchUpInside. You can easily change that to another event if you prefer.
It is hardcoded to look for 3 taps. This could be made more general.
I would simply try the following, without even subclassing.
I would use the tag of each button to multiplex 2 informations: number of repetition of taps, time since last tap.
Multiplexed value could be 1000_000 * nbTaps + time elapsed (measured in tenths of seconds) since the beginning of the play (that will work for more than a full day).
So, a second tap 10 minutes after beginning of play will set the tag to 2000_000 + 6000 = 2006_000
When creating buttons (at beginning of play, in viewDidLoad), set their tag to 0.
In the IBAction,
demultiplex the tag to find nbTaps and timeOfLastTap
compute the new value of timeElapsed (in tenths of seconds)
if difference with timeOfLastTap is more than 10 (1 second), reset tag to 1000_000 + timeElapsed (so nbTaps is 1) and return
if difference is less than 10, test nbTaps (as stored in tag)
if less than 2, then just reset tag to (nbTaps + 1) * 1000_000 + timeElapsed and return
if nbTaps >= 2, here you are.
reset tag to timeElapsed (in tenths of seconds) (nbTaps is now zero)
perform the IBAction you need.
I've a subclass of UIPageControl, and I'd like to observe the changes in currentPage.
Unfortunately, my currentPage's didSet isn't called when currentPage changes from taps on MyPageControl.
class MyPageControl: UIPageControl {
override var currentPage: Int {
didSet {
// not called when `currentPage` is changed from a tap
updateSomething(for: currentPage)
}
}
}
And the change can't be observed using KVO either:
class MyPageControl: UIPageControl {
var observation: NSKeyValueObservation?
override func awakeFromNib() {
super.awakeFromNib()
observation = observe(\.currentPage) { (_, _) in
// not called when `currentPage` is changed from a tap
updateSomething(for: self.currentPage)
}
}
}
From Apple Doc : Apple Doc
When a user taps a page control to move to the next or previous page, the control sends the valueChanged event for handling by the delegate. The delegate can then evaluate the currentPage property to determine the page to display. The page control advances only one page in either direction. The currently viewed page is indicated by a white dot. Depending on the device, a certain number of dots are displayed on the screen before they are clipped.
Code:
pageControl.addTarget(self, action:#selector(pageControlTapHandler(sender:)), for: .touchUpInside)
//or
pageControl.addTarget(self, action:#selector(pageControlTapHandler(sender:)), for: .valueChanged)
...
func pageControlTapHandler(sender:UIPageControl) {
print("currentPage:", sender.currentPage) //currentPage: 1
}
You may detect the taps with sendAction(_:to:for:) and read the value of currentPage at that moment.
class MyPageControl: UIPageControl {
override func sendAction(_ action: Selector, to target: Any?, for event: UIEvent?) {
super.sendAction(action, to: target, for: event)
updateSomething(for: currentPage)
}
}
How can I count time on how long the button is pressed using UILongPressGestureRecognizer; I am looking to print the long-Pressed count time in the displayLabel.text
I've tried most possible way.
#IBOutlet weak var buttonPressed: UIButton!
#IBOutlet weak var displayLabel: UILabel!
var buttonPressedCount : Int = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let longPressObj = UILongPressGestureRecognizer(target: self, action: #selector(longPressButton))
longPressObj.minimumPressDuration = 2
longPressObj.numberOfTouchesRequired = 1
self.view.addGestureRecognizer(longPressObj)
// below statement is not a right one but i tried many possiblity including this one.
if longPressObj.minimumPressDuration == TimeInterval(2) {
displayLabel.text = "Longpressed for 2 seconds"
} else if longPressObj.minimumPressDuration == TimeInterval(3) {
displayLabel.text = "3"
} else if longPressObj.minimumPressDuration == TimeInterval(3) {
displayLabel.text = "4"
}
}
#IBAction func longPressButton() {
displayLabel.text = "Button pressed for \(buttonPressedCount)"
}
I want to display time of the long-Pressed NOT BUTTON CLICKED.
enter image description here
Thank you in advance!
EDITS:-
1. I just want to show the Running duration while the user is performed Long-Press. I would really appreciate the real-time count in the
2. Also is it will be helpful to show the total duration after stop pressing.
(https://i.stack.imgur.com/ppr0W.png)
Your target function should include sender, then you can get the state of the UILongPressGestureRecognizer.
Here is the Apple official document.
First save the gesture begin time, then you can use the current time to subtract the begin pressed time to get the duration in state .ended (or/and .cancelled, .failed).
Sample code:
class ViewController: UIViewController {
var touchBeginTime: Double = 0
var touchCountTimer: Timer?
override func viewDidLoad() {
super.viewDidLoad()
let longPressObj = UILongPressGestureRecognizer(target: self, action: #selector(longPressButton(sender:)))
longPressObj.minimumPressDuration = 2
longPressObj.numberOfTouchesRequired = 1
self.view.addGestureRecognizer(longPressObj)
}
#IBAction func longPressButton(sender: UILongPressGestureRecognizer) {
switch sender.state {
case .began:
touchCountTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true, block: { (timer) in
print("User pressing \(Date().timeIntervalSince1970 - self.touchBeginTime) sec.")
})
touchBeginTime = Date().timeIntervalSince1970
break;
case .changed:
//print("Long pressed changed \(Date().timeIntervalSince1970 - touchBeginTime)")
break;
case .ended, .cancelled, .failed:
touchCountTimer?.invalidate()
print("User pressed total \(Date().timeIntervalSince1970 - touchBeginTime) sec.")
break;
default:
break;
}
}
}
Since you want to show the running duration while the long press is being performed, you need to use a timer.
Start the timer when the long press reaches the .began state and stop (invalidate) the timer when the long press reaches the .ended or .canceled state.
Your time should repeat every second and update the label based on the difference between the current date and the date the long press began.
Rather than using a long press gesture recognizer, why not attach button actions to the touchDownInside event and the touchUpInside and touchUpOutside events on your button. your touchDownInside code can start your timer, which would update the label. The touchUpInside/touchUpOutside action would stop the timer.
I want to move the UISlider knob to the middle, or 0.0 value in my case, when the double tap event is occurred on it.
Knob is moving to 0.0 on double tap but immediately it resume to the old position/value.
I have tried the below code. Any help would be much appreciated.
override func viewDidLoad() {
super.viewDidLoad()
let eqSlider = UISlider(frame:CGRectMake(0, 0, 160, 40))
eqSlider.minimumValue = -12.0
eqSlider.maximumValue = 12.0
eqSlider.value = 0.0
self.view.addSubview(eqSlider)
// detecting double tap on slider method
eqSlider.addTarget(self, action: #selector(EQViewController.doubleTappedSlider(_:event:)), forControlEvents: .TouchDownRepeat)
}
func doubleTappedSlider(sender: UISlider, event: UIEvent) {
if let firstTouchObj = event.allTouches()?.first {
if 2 == firstTouchObj.tapCount {
sender.value = 0.0
}
}
}
The problem is that the two taps in your double tap will also be acted upon by the slider itself, so after you set its position manually, the user is setting it straight back.
To avoid this, you can add the following line after you set the .value:
sender.cancelTracking(with: nil)
This will "cancel any ongoing tracking" as stated in the documentation.
I am trying to print a value from a slider at regular intervals. But only print the value if it is different to that last printed. I also do not want to miss any of the output values from the slider.
To do this I have created an array and added an element to the start of that array if it is different to the one already at the start. I have then used a repeating NSTimer to regularly call a function that prints the last element in the array before removing it from the array.
What happens when I run the app is the NSTimer stops anything being printed for it's set time, but then all of the elements print at once and more than one of each print. I've tried messing about with lots of different things - this is the closest I have got to making it work.
If you need to know any more info let me know.
I really appreciate any help given, thanks very much.
var sliderArray: [Float] = []
var timer: NSTimer!
let step: Float = 1
#IBAction func sliderValueChanged(sender: AnyObject)
{
let roundedValue = round(slider.value / step) * step
slider.value = roundedValue
if sliderArray.first != slider.value
{
sliderArray.insert(slider.value, atIndex: 0)
}
NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(sendSliderPosition), userInfo: nil, repeats: true)
}
func sendSliderPosition()
{
if sliderArray.count > 0
{
print(self.sliderArray.last)
sliderArray.removeLast()
}
}
I would suggest using CADisplayLink. A CADisplayLink object is a timer object that allows your application to synchronize its drawing to the refresh rate of the display. Which is perfect for your slider case.
This will also not trigger unnecessary call when the slider or the UI is at rest.
class C: UIViewController {
var displayLinkTimer: CADisplayLink?
#IBOutlet weak var slider: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
displayLinkTimer = CADisplayLink(target: self, selector: #selector(printSliderValue))
let runLoop = NSRunLoop.mainRunLoop()
displayLinkTimer?.addToRunLoop(runLoop, forMode: runLoop.currentMode ?? NSDefaultRunLoopMode )
displayLinkTimer?.paused = true
}
override func viewWillDisappear(animated: Bool) {
super.viewWillDisappear(animated)
displayLinkTimer?.paused = true
}
deinit {
displayLinkTimer?.invalidate()
}
func printSliderValue()
{
let step: Float = 1
let roundedValue = round(slider.value / step) * step
slider.value = roundedValue
print(roundedValue)
}
}
The basic idea is this:
--> Every time the screen needs to redraw (this will happen at max around 60 frames per second taking into consideration this is fps rate), we get a chance to perform function.
--> to do so, we add the displayLink to the Run Loop. (Run lopp processes input/ refreshes UI and time slices)
--> NOTE This method wont be called if there is no redraw needed on the screen. This is not a timer per say that fires periodically. It fires when redraw is needed. In Sliders case, we want this to fire when we move slightest of the slider too.
For more info on how it actually works try it out and see the apple documentation. Make sure to invalidate before deinitializing the ViewController.
Figured out the answer, thank to everyone for the help and suggestions:
override func viewDidLoad()
{
super.viewDidLoad()
NSTimer.scheduledTimerWithTimeInterval(0.02, target: self, selector: #selector(sendSliderPosition), userInfo: nil, repeats: true)
}
#IBAction func sliderValueChanged(sender: AnyObject)
{
let step: Float = 1
let roundedValue = round(slider.value / step) * step
slider.value = roundedValue
if sliderArray.first != slider.value
{
sliderArray.insert(slider.value, atIndex: 0)
}
}
func sendSliderPosition()
{
if sliderArray.count > 1
{
let end1 = sliderArray.count-2
print(sliderArray[end1])
sliderArray.removeLast()
}
}
Explanation:
If the new slider value is different to the one already in the array then add it to the array at the start. Use an NSTimer to repeatedly call the sendSliderPosition function from viewDidLoad. The function will only be performed if there is more than one element in the array. If there is, print the element before the last one and remove the last. This always ensures that there is one element in the array so the function does not always run and that the element printed is the most recent one that hasn't already been printed.