Reputation: 38025
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.
Upvotes: 7
Views: 4928
Reputation: 1381
Riffing off @AechoLiu's answer, here is how I implemented this in a UIButton subclass:
// Properties
private lazy var disabledButton: UIButton = {
let disabledButton = UIButton(frame: frame)
disabledButton.backgroundColor = .clear
disabledButton.addTarget(self, action: #selector(disabledButtonTouchUpInside(_:)), for: .touchUpInside)
return disabledButton
}()
// Method overrides
override func awakeFromNib() {
super.awakeFromNib()
// Do any other set up you need
if let superview = superview {
// Place the disabled button behind self in the hierarchy.
// When `self` is disabled it will pass its touches through.
superview.insertSubview(disabledButton, belowSubview: self)
}
}
override func layoutSubviews() {
super.layoutSubviews()
// Sync `disabledButton` frame to main button's
disabledButton.frame = frame
}
// Button handling
@objc private func disabledButtonTouchUpInside(_ sender: UIButton) {
// Handle as required. You could:
// - Trigger the button's normal action using sendActions(for: .touchUpInside).
// - Add a delegate protocol to the button subclass and triggered one of its methods here.
}
Upvotes: 0
Reputation: 13234
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://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.
Upvotes: -8
Reputation: 18418
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.
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)
}
}
Upvotes: 2
Reputation: 385930
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.
Upvotes: 22