I wanted to add a single tap gesture to a UITextView so that when my save dialog is opened I can cancel it by tapping the UITextView. However when I do that it cancels the normal single tap behavior of the textview. Now when I single tap on the text without the save drawer out it does nothing. The tap does trigger with the callback method correctly but it cancels the default action of the UITextView, (to place the cursor wherever you tapped).
// Add gesture to cancel saving
let singleCodeTextViewTap = UITapGestureRecognizer(target: self, action: Selector("codeTextViewTapped"))
singleCodeTextViewTap.numberOfTapsRequired = 1;
codeTextView.addGestureRecognizer(singleCodeTextViewTap)
func codeTextViewTapped() {
if saveDrawerOut {
moveFilenameBannerOffScreen()
saveAsTextField.text = ""
}
}
How can I enable the default behavior of the UITextView while having a single tap gesture?
EDIT
I tried adding cancelsTouchesInView = false and that does not seem to work although it would seem like the correct answer. So now my code looks like this:
singleCodeTextViewTap = UITapGestureRecognizer(target: self, action: Selector("codeTextViewTapped"))
singleCodeTextViewTap.numberOfTapsRequired = 1
singleCodeTextViewTap.cancelsTouchesInView = false
codeTextView.addGestureRecognizer(singleCodeTextViewTap)
Set the delegate of singleCodeTextViewTap to your view controller, implement gestureRecognizerShouldBegin: and return false if saveDrawerOut is true.
You could also set the cancelsTouchesInView to false. So it would be
singleCodeTextViewTap.cancelsTouchesInView = false
This would allow the gesture to be recognized and forwarded to your UITextView
Try singleCodeTextViewTap.cancelsTouchesInView = false
It should let multiple gesture recognizers live in harmony with each other.
Related
I have a view controller in which a user can move around UIButton, UISlider, and a custom UIView based control by panning the control around the screen. This view controller is used to create custom layout of control by the user. This is all done by adding PanGestureRecognizer to the UIControl to move the position of the control relative to user's finger location.
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(pan))
panRecognizer.delegate = self;
myslider.addGestureRecognizer(panRecognizer)
~~~~~~~~~~~~
//pan handler method
#objc func pan(_ gestureRecognizer: UIPanGestureRecognizer) {
let translation = gestureRecognizer.translation(in: view)
guard let gestureView = gestureRecognizer.view else {
return
}
//Move the center to user's finger location
gestureView.center = CGPoint(x: gestureView.center.x + translation.x, y: gestureView.center.y + translation.y);
gestureRecognizer.setTranslation(.zero, in: view);
if (gestureRecognizer.state == .ended) {
//Save the new location inside data. Not relevant here.
}
}
This worked fine in iOS 13 and below for all the control i mentioned above (with the UISlider being a bit glitchy but it still responded to the pan gesture and i don't need the value of the uislider anyway so it's safe to ignore). However testing the app in iOS 14 reveals that UISlider completely ignore the PanGesture (proven by adding breakpoint that never got called inside the pan gesture handling method).
I have looked at apple's documentation regarding UISlider and found no change at all related to gesture handling so this must be done deliberately in deeper lever. My question is: is there any way to "force" my custom gesture to be executed instead of UISlider's gesture without the need to create transparent button overlay (which i don't know will work or not) / creating dummy slider just for this ?
Additionally i also added UILongPressGestureRecognizer and UITapGestureRecognizer to the control. Which worked fine on other UIButton but completely ignored by the UISlider in iOS14 (in iOS 13 everything worked fine).
Thanks.
Okay.. I found the answer myself after some more digging and getting cue from "claude31" about making a new clean project and testing from there.
The problem was with the overriden beginTracking function. This UISlider of mine is actually subclassed into a custom class and in there the beginTracking function is overriden, as per code below:
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let percent = Float(touch.location(in: self).x / bounds.size.width)
let delta = percent * (maximumValue - minimumValue)
let newValue = minimumValue + delta
self.setValue(newValue, animated: false)
super.sendActions(for: UIControl.Event.valueChanged)
return true
}
This is to make the slider move immediately to the user finger location if the touch is inside the boundaries of the UISlider (without the need to first touch the thumbTrack and sliding it to the position the user wants).
In iOS 13 this function does not block the gestureRecognizer from getting recognized. However in iOS 14B4, overriding this function, with sendActions(for:) method inside it cause the added gestureRecognizer to be ignored completely, be it pan gesture, long press gesture, or even tap gesture.
For me, the solution is to simply add a state to check whether the pan gesture is required in this view controller or not. Because, luckily, i only need the added gestureRecognizer in view controller that does not require the beginTouch function to be customized and vice versa.
Edit:
Originally i wrote that the cause of the problem is due to always true return value, however, I just reread the documentation and the default return value for this function is also true. So i think the root causes of this problem is the sendActions(for:) method, causing the added gestureRecognizer to be ignored. My Answer above has been edited to reflect this.
I want to do something when the focused voice-over element is being tapped again.
Function accessibilityElementDidBecomeFocused() will only get called when the element is focused for first time.
When we are again single tapping on the same focused element then this function won't get called. Can anybody suggest some solution?
Can anybody suggest some solution?
Here are some ideas on the fly to detect a single-tap on the same focused element:
Create a variable nbSelections that will count the number of single-taps.
Create a tap gesture recognizer on your element to increment the number of taps for instance:
let tap = UITapGestureRecognizer(target: self,
action: #selector(addTapCounter(info:)))
tap.numberOfTapsRequired = 1
self.addGestureRecognizer(tap)
Add the trait that will allow to catch the simple tap directly on the element:
override var accessibilityTraits: UIAccessibilityTraits {
get { return .allowsDirectInteraction }
set { }
}
Set nbSelections = 0 when the element loses the focus:
override open func accessibilityElementDidLoseFocus() { nbSelections = 0 }
Combining these ideas with the UIAccessibilityFocus informal protocol could be a good line of research to reach your goal.
However, this technical solution assumes that the single-tap is made directly on the element itself (-trait specific) and not anywhere else (I don't see how to catch this event the top off my head).
Is there any official way how to set UIBarButtonItem.enabled property? I tried to set a backButtonItem in previous controller. But enabled property is ignored.
More in this simple example project.
I don't want to some solution like "make your own leftBarButtonItem and set its alpha ..."
Edit: I don't want to hide it, only disable it with dimmed colour and disabled user interaction. It's exactly the same behaviour as for disabled leftBarButtonItem.
As of today it is not possible to disable the back button using the enabled property. The backBarButtonItem property will be nil unless you create a custom item and even then it will ignore the enabled property. There are a couple (non-satisfactory) ways around this.
Hide the button
This is what Apple wants you to do given that they ignore the enabled property. It is as simple as
navigationItem.hidesBackButton = true
and should be the preferred approach unless you have good reasons.
Disable and Tint the Navigation Bar
You can disable user interaction on the whole navigation bar and tint it to make the back button appear disabled.
navigationController?.navigationBar.isUserInteractionEnabled = false
navigationController?.navigationBar.tintColor = UIColor.lightGray
This does, unfortunately, affect other elements in the navigation bar as well so it might not be an option if, for instance, you have another bar button item on the right side.
Use a Custom Left Bar Button Item
The leftBarButtonItem does not ignore the enabled property so you could create a custom item and trigger the pop manually when it is activated.
navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Cancel", style: .plain, target: self, action: #selector(ThisClass.backButtonTapped))
...
navigationItem.leftBarButtonItem?.isEnabled = false
func backButtonTapped() {
self.navigationController?.popViewController(animated: true)
}
This will, however, not have the back bar button style with the leading triangular indicator.
Add below code in your ViewController2.swift Class.
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.hidesBackButton = true;
}
It will hide your back button.
If you want to hide it, UInavigationItem has a hidesBackButton property.
I know this is an old thread, but this may help someone else.
As mentioned by hennes, you can no longer disable the back button. Instead, you will need to disable the entire navigationBar.
The approach I took, was disabling the navigationBar, and then applying an 0.5 alpha to the subviews of the navigation bar.
In your ViewController class:
func changeBarButtons(alpha: CGFloat) {
navigationController?.navigationBar.subviews.forEach { firstViews in
firstViews.subviews.forEach { view in
if ["_UIButtonBarButton", "_UIButtonBarStackView"].contains(type(of: view).description()) {
view.alpha = alpha
}
}
}
}
func set(loading: Bool) {
let alpha: CGFloat = loading ? 0.5 : 1
navigationController?.navigationBar.isUserInteractionEnabled = !loading
changeBarButtons(alpha: alpha)
}
Keep in mind, that Apple could change the names of the class any time. That being said, it's highly unlikely they do so. If you don't mind the title of the View Controller fading out, you can apply the alpha to all the subviews, without checking the class name.
Don't try to disable your custom back button (won't work), just set a new one which is disabled. You can reach the previous navigation item through the UINavigationBar.backItem property.
// set disabled back button
let backButton = UIBarButtonItem(title: "Back", style: UIBarButtonItem.Style.plain, target: nil, action: nil)
backButton.isEnabled = false
navigationController?.navigationBar.backItem?.backBarButtonItem = backButton
// disable pop gesture
navigationController?.interactivePopGestureRecognizer?.isEnabled = false
In one function in my Swift code, I need to wait until user presses a button. Otherwise, I just do nothing. How do I do this? Maybe button press triggers some event I can catch.
P.S. Due to structure of my code, doing what I want on this button press (like #IBAction) is not an option!
Declare a global boolean value such as var buttonPressed = false and then, in the IBAction of the button (When you click it):
if buttonPressed = false{
buttonPressed = true
}
And in the method you want to call when the button is pressed, just ask if buttonPressed is true.
Try that and tell me if it worked
You can set the user interaction to false by default of your main UIView (usually the UIViewController view)
self.view.userInteractionEnabled = false
and when you have the action of button you set the user interaction to true.
self.view.userInteractionEnabled = true
I might be too late but personally, there are two way you can achieve this.
Add Observer
Use delegate pattern so whenever user tap a button you notify your delegate
I have a UIButton on a form, and want to put it in a disabled state when the form is incomplete. However, I still want to be able to detect if a user attempts to press the button even in its disabled state so that the interface can let the user know that certain required fields on the form are not filled-in yet (and perhaps scroll to that field and point it out, etc.).
There doesn't seem to be any straightforward way to do this. I tried simply attaching a UITapGestureRecognizer to the UIButton but it doesn't respond when the button is in a disabled state.
I'd like to avoid subclassing UIButton if possible, unless it's the only way.
Create a fallback button. Put it behind the main button. Set its background and text colors to [UIColor clearColor] to ensure it won't show up. (You can't just set its alpha to 0 because that makes it ignore touches.) In Interface Builder, the fallback button should be above the main button in the list of subviews, like this:
Give it the same frame as the main button. If you're using autolayout, select both the main and fallback buttons and create constraints to keep all four edges equal.
When the main button is disabled, touches will pass through to the fallback button. When the main button is enabled, it will catch all the touches and the fallback button won't receive any.
Connect the fallback button to an action so you can detect when it's tapped.
Based on #rob idea, I sub-class a UIButton, and add a transparent button before someone addSubview on this button.
This custom UIButton will save many time about adjusting the UI components on the storyboard.
Update 2018/08
It works well, and add some enhanced detail to this sub-class. I have used it for 2 years.
class TTButton : UIButton {
// MARK: -
private lazy var fakeButton : UIButton! = self.initFakeButton()
private func initFakeButton() -> UIButton {
let btn = UIButton(frame: self.frame)
btn.backgroundColor = UIColor.clearColor()
btn.addTarget(self, action: #selector(self.handleDisabledTouchEvent), forControlEvents: UIControlEvents.TouchUpInside)
return btn
}
// Respect this property for `fakeButton` and `self` buttons
override var isUserInteractionEnabled: Bool {
didSet {
self.fakeButton.isUserInteractionEnabled = isUserInteractionEnabled
}
}
override func layoutSubviews() {
super.layoutSubviews()
// NOTE: `fakeButton` and `self` has the same `superView`.
self.fakeButton.frame = self.frame
}
override func willMoveToSuperview(newSuperview: UIView?) {
//1. newSuperView add `fakeButton` first.
if (newSuperview != nil) {
newSuperview!.addSubview(self.fakeButton)
} else {
self.fakeButton.removeFromSuperview()
}
//2. Then, newSuperView add `self` second.
super.willMoveToSuperview(newSuperview)
}
#objc private func handleDisabledTouchEvent() {
//NSLog("handle disabled touch event. Enabled: \(self.enabled)")
self.sendActionsForControlEvents(.TouchUpInside)
}
}
You have a great misunderstanding of user experience.
If a button is disabled, it is meant to be non-interactable.
You can not click on a disabled button, that is why it is disabled.
If you want to warn users about something when that button is clicked (e.g. form not filled correctly or completely), you need to make that button enabled. And just warn users when they click on it, instead of proceeding further with app logic.
Or you can keep that button disabled until form criteria are met, but show what is wrong with the form using another way, like putting exclamation marks near text fields, changing text field colors to red, or something like that...
But never try to add gesture recognizers, or hidden fallback buttons to a disabled button.
Check those and let me know if you see a disabled button:
https://airbnb.com/signup_login
https://spotify.com/us/signup/
https://netflix.com/signup/regform
https://reddit.com/register/
https://twitter.com/signup
https://facebook.com/r.php
https://appleid.apple.com/account
https://accounts.google.com/SignUp
https://login.yahoo.com/account/create
https://signup.live.com/signup
All the proceed buttons on these websites are always enabled, and you get feedback about what is wrong when you try to continue.
And here is really good answer: https://ux.stackexchange.com/a/76306
Long story short: disabled UI elements meant to be not-interactable.
Trying to make them interactable while they are disabled is the same to making them enabled in the first place.
So, for your question's case, it is just a styling issue. Just try styling your button, instead of making it disabled/enabled.