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.
Related
I am trying to implement one button- one click to display the first message, the second click would change to second message, the third click would change to the third message.
I looked up to one possible solution is to use UITapGestureRecognizer - single tap and double tap, which means I can trigger the button (single tap to display the first message and double tap to display the second message).
However, if I have more than two lines and I just want to display them by clicking each one (like animation). Would that be possible to just deal with that inside one UIView and UIbutton?
I currently have 3 simple messages:
#IBOutlet weak var Textfield: UITextView!
#IBOutlet weak var Changingbutton: UIButton!
#IBAction func ChangingTapped(_ btn: UIButton) {
Textfield.text = "Changing to driving"
Textfield.text = "Changing to walking"
Textfield.text = "Changing to cycling"
}
The problem now is when I click the button it would just go the last message. That might not be a clever way to do so.
Thanks so much for the inputs and I am sorry if that is a rather simple question.
You can implement a custom CaseIterable enumeration to perform a loop so that you can get the next element every time you press a button:
extension CaseIterable where Self: Equatable {
var allCases: AllCases { Self.allCases }
var nextCase: Self {
let index = allCases.index(after: allCases.firstIndex(of: self)!)
guard index != allCases.endIndex else { return allCases.first! }
return allCases[index]
}
#discardableResult
mutating func next() -> Self {
self = nextCase
return self
}
}
Create a enumeration with your transportation modes:
enum Mode: String, CaseIterable {
case cycling, driving, walking
}
add a mode property to your view controller and set the initial value
var mode: Mode = .cycling
Now you can simply call the next mode method every time you press the button:
func ChangingTapped(_ btn: UIButton) {
Textfield.text = "Changing to " + mode.next().rawValue
}
Note: It is Swift naming convention to name your methods and properties starting with a lowercase letter.
Why not just set a counter and increment it every time your IBAction is activated?
var x = 0
#IBAction func ChangingTapped(_ btn: UIButton) {
if(x==0){
Textfield.text = "Changing to driving"
}
else if(x==1){
Textfield.text = "Changing to walking"
}
else{
Textfield.text = "Changing to cycling"
}
x +=1
//if x needs to be reset
/*
if(x > 2) x = 0
*/
}
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)
}
}
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)")
}
}
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.
As we know, the original keyboard in iOS can delete whole words by holding down the delete button (⌫) for an extended period of time.
So how can we use the same functionality for custom keyboards in Swift, iOS 8?
Note:
I'm currently using proxy.deleteBackward() for deleting letters, and using:
var gesture = UILongPressGestureRecognizer(target: self, action: "longPressed:")
gesture.minimumPressDuration = 1.0
button.addGestureRecognizer(gesture)
when the button is pressed for a greater amount of time.
Thanks!
I'm not sure how you would be able to do it through gesture recognizer.
The original keyboard behaviour is,
When the button is pressed and kept pressed for initial X-time
interval, it keeps deleting backward.
When the button is kept
pressed after the initial X-time interval, it starts deleting words
instead of just characters.
After the button is pressed the first time, you should probably keep calling your delete function and keep noticing if 'X-time-interval' has elapsed. Pseudocode would be
var startTime: NSDate = NSDate()
var timer: NSTimer?
func deleteButtonPressed(deleteButton: UIButton) {
startTime = NSDate()
timer = NSTimer.scheduledTimerWithTimeInterval(0.4, target: self, selector: Selector("delete"), userInfo: nil, repeats: true)
}
func delete() {
if !deleteButton.highlighted {
timer.invalidate()
timer = nil
return
}
if ((currentNSDate - startTime ) < "X-time-Interval") {
// delete backward
} else {
/* figure out last space character in text and create NSRange
then
mytextView.text deleteCharactersInRange:theRange
set new text */
}
}