I'm working with an app in prototype stage of development. Some interface elements do not have any action assigned to them either through storyboard or programmatically.
According to UX guidelines, I want to find these "inactive" buttons in app and have them display a "feature not available" alert when tapped during testing. Can this be done through an extension of UIButton?
How can I assign a default action to UIButton to show an alert unless another action is assigned via interface builder or programmatically?
Well what you are trying to achieve can be done. I have done this using a UIViewController
extension and adding a closure as the target of a button which does not have a target. In case the button does not have an action an alert is presented.
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
self.checkButtonAction()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
@IBAction func btn_Action(_ sender: UIButton) {
}
}
extension UIViewController{
func checkButtonAction(){
for view in self.view.subviews as [UIView] {
if let btn = view as? UIButton {
if (btn.allTargets.isEmpty){
btn.add(for: .touchUpInside, {
let alert = UIAlertController(title: "Test 3", message:"No selector", preferredStyle: UIAlertControllerStyle.alert)
// add an action (button)
alert.addAction(UIAlertAction(title: "OK", style: UIAlertActionStyle.default, handler: nil))
// show the alert
self.present(alert, animated: true, completion: nil)
})
}
}
}
}
}
class ClosureSleeve {
let closure: ()->()
init (_ closure: @escaping ()->()) {
self.closure = closure
}
@objc func invoke () {
closure()
}
}
extension UIControl {
func add (for controlEvents: UIControlEvents, _ closure: @escaping ()->()) {
let sleeve = ClosureSleeve(closure)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
objc_setAssociatedObject(self, String(format: "[%d]", arc4random()), sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
I have tested it. Hope this helps. Happy coding.
As you cannot override methods in extensions, the only remaining options are:
1. subclass your buttons - but probably not what you're looking for, because I assume you want to use this functionality for already existing buttons
2. method swizzling - to change the implementation of existing function, i.e. init
How can I assign a default action to UIButton to show an alert unless another action is assigned via interface builder or programmatically?
I would suggest to do method swizzling:
Through swizzling, the implementation of a method can be replaced with a different one at runtime, by changing the mapping between a specific selector(method) and the function that contains its implementation.
https://www.uraimo.com/2015/10/23/effective-method-swizzling-with-swift/
Remark: I would recommend to check the article above.
As an exact answer for your question, the following code snippet should be -in general- what are you trying to achieve:
class ViewController: UIViewController {
// MARK:- IBOutlets
@IBOutlet weak var lblMessage: UILabel!
// MARK:- IBActions
@IBAction func applySwizzlingTapped(_ sender: Any) {
swizzleButtonAction()
}
@IBAction func buttonTapped(_ sender: Any) {
print("Original!")
lblMessage.text = "Original!"
}
}
extension ViewController {
func swizzleButtonAction() {
let originalSelector = #selector(buttonTapped(_:))
let swizzledSelector = #selector(swizzledAction(_:))
let originalMethod = class_getInstanceMethod(ViewController.self, originalSelector)
let swizzledMethod = class_getInstanceMethod(ViewController.self, swizzledSelector)
method_exchangeImplementations(originalMethod, swizzledMethod)
}
func swizzledAction(_ sender: Any) {
print("Swizzled!")
lblMessage.text = "Swizzled!"
}
}
After calling swizzleButtonAction()
-by tapping the "Apply Swizzling" button (applySwizzlingTapped
)-, the selector of "Button" should changed from buttonTapped
to swizzledAction
.
Output:
I would implement this kind of function by using IBOutlet collection variable, and then removing buttons from that collection as soon as the feature is implemented. something like this:
class ViewController {
@IBOutlet var inactiveBtns: [UIButton]!
func viewDidLoad() {
inactiveBtns.forEach { (button) in
button.addTarget(self, action: #selector(showDialog), for: UIControlEvents())
}
}
func showDialog() {
// show the dialog
}
}
That IBOutlet is visible in the interface builder, so you can add multiple buttons in there.
来源:https://stackoverflow.com/questions/45160145/ios-swift-how-to-assign-a-default-action-to-all-buttons-programmatically