How to Make #Selector Refer to a Closure in Swift

Can I make #selector refer to a closure in Swift?

As @gnasher729 notes, this is not possible because selectors are just names of methods, not methods themselves. In the general case, I'd use dispatch_after here, but in this particular case, the better tool IMO is UIView.animateWithDuration, because it's exactly what that function is for, and it's very easy to tweak the transition:

UIView.animateWithDuration(0, delay: 0.5, options: [], animations: {
self.view.backgroundColor = UIColor.whiteColor()
self.view.alpha = 1.0
}, completion: nil)

How to pass closure as a parameter in perform(selector, withObject)

Your closure is not an Objective-C block, so it can't be passed through the ObjC runtime. You have to mark it as a block using @convention.

let closure: @convention(block) () -> Void = { ... }

You can convert an existing closure into a block by assigning it:

let closure = { ... }
let block: @convention(block) () -> Void = closure
perform(#selector(foo(param:)), with: block)

Objective-C blocks are actually objects, and participate in ARC. The crash occurs because perform tries to call Block_copy on a non-block.

Of course, as a rule selectors are not the right tools in Swift, and you should convert any selector-based interface to just take a function argument in the first place. If you find yourself using perform, you're probably on the wrong road in Swift. But it is still available if you need it.

The #selector is not compatible with the closure?

A selector is a string that's used to identify methods, properties, initializers in the Objective C runtime. When you use a notation like #selector(SomeClass.SomeMethod(withParam:AndParam:), you're specifying the selector in a format that the compiler can parse easily and verify to be correct. But ultimately, that would just be reduced down into a C string like: "SomeMethodwithParam:AndParam:".

Essentially, every class has a dictionary which maps selectors to the function pointers of the code that implements them. When a selector is used to invoke a function, the Objective C runtime searches the method table for the class in question, and looks up the method implementation corresponding to the given selector.

There's no way for this process to work with closures, which are anonymous by definition. Thus, you can only use selectors to refer to methods, properties, initializers that are registered with the Objective C runtime (which is what @objc does, implicitly or explicitly).

Passing closure in swift as parameter to be used by selector in function

The problem is that the target-action mechanism is an Objective-C mechanism, and therefore is predicated on the notion that the action selector is a method of an object. You need, therefore, to have some NSObject-based object that has this function as a method, and which can then serve as the target.

Thus, if what differs in every case is the target and the action, what you need to pass is a reference to the target along with the selector string. Swift will squawk at this, but if you know how to form a selector string correctly you can certainly get away with it; you just won't be able to use the #selector syntax, and so you will risk crashing if you form the selector string incorrectly. But it's the kind of thing we used to do all the time in the old Objective-C days, so go right ahead if that's your aim.

Totally artificial but working example:

func buttonMaker(target:NSObject, selectorString:String) -> UIButton {
let b = UIButton(type:.system)
b.setTitle("Testing", for: .normal)
b.addTarget(target, action: Selector(selectorString), for: .touchUpInside)
b.sizeToFit()
return b
}

And here's how to call it from a view controller:

func doButton(_ sender:Any) {
print("ha!")
}

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
let b = buttonMaker(target:self, selectorString:"doButton:")
b.frame.origin = CGPoint(x:100, y:100)
self.view.addSubview(b)
}

And when we tap the button, we don't crash (rather, we print "ha"), because I know how to make selector strings correctly. But, as you can see, to accomplish this I had to give up the use of #selector altogether, so safety is out the window. If I had written my selector string incorrectly — for instance, if I had spelled it wrong, or omitted the colon — we'd have crashed on the button tap, just like we used to all the time before Swift #selector and Objective-C @selector were invented.

Adding a closure as target to a UIButton

Do Not Use This Answer, See Note Below

NOTE:
like @EthanHuang said
"This solution doesn't work if you have more than two instances. All actions will be overwrite by the last assignment."

Keep in mind this when you develop, i will post another solution soon.

If you want to add a closure as target to a UIButton, you must add a function to UIButton class by using extension

Swift 5

import UIKit    
extension UIButton {
private func actionHandler(action:(() -> Void)? = nil) {
struct __ { static var action :(() -> Void)? }
if action != nil { __.action = action }
else { __.action?() }
}
@objc private func triggerActionHandler() {
self.actionHandler()
}
func actionHandler(controlEvents control :UIControl.Event, ForAction action:@escaping () -> Void) {
self.actionHandler(action: action)
self.addTarget(self, action: #selector(triggerActionHandler), for: control)
}
}

Older

import UIKit

extension UIButton {
private func actionHandleBlock(action:(() -> Void)? = nil) {
struct __ {
static var action :(() -> Void)?
}
if action != nil {
__.action = action
} else {
__.action?()
}
}

@objc private func triggerActionHandleBlock() {
self.actionHandleBlock()
}

func actionHandle(controlEvents control :UIControlEvents, ForAction action:() -> Void) {
self.actionHandleBlock(action)
self.addTarget(self, action: "triggerActionHandleBlock", forControlEvents: control)
}
}

and the call:

 let button = UIButton()
button.actionHandle(controlEvents: .touchUpInside,
ForAction:{() -> Void in
print("Touch")
})


Related Topics



Leave a reply



Submit