问题
I have a generic control class which needs to set the completion of the button depending on the view controller.Due to that setLeftButtonActionWithClosure function needs to take as parameter a closure which should be set as action to an unbutton.How would it be possible in Swift since we need to pass the function name as String to action: parameter.
func setLeftButtonActionWithClosure(completion: () -> Void)
{
self.leftButton.addTarget(<#target: AnyObject?#>, action: <#Selector#>, forControlEvents: <#UIControlEvents#>)
}
回答1:
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: UIControlEvents.TouchUpInside,
ForAction:{() -> Void in
print("Touch")
})
回答2:
Similar solution to those already listed, but perhaps lighter weight:
@objc class ClosureSleeve: NSObject {
let closure: ()->()
init (_ closure: @escaping ()->()) {
self.closure = closure
}
@objc func invoke () {
closure()
}
}
extension UIControl {
func addAction(for controlEvents: UIControl.Event = .touchUpInside, _ closure: @escaping ()->()) {
let sleeve = ClosureSleeve(closure)
addTarget(sleeve, action: #selector(ClosureSleeve.invoke), for: controlEvents)
objc_setAssociatedObject(self, "[\(arc4random())]", sleeve, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
Usage:
button.addAction {
print("Hello, Closure!")
}
or
button.addAction(for: .touchUpInside) {
print("Hello, Closure!")
}
Or if avoiding retain loops:
self.button.addAction(for: .touchUpInside) { [unowned self] in
self.doStuff()
}
回答3:
You can effectively achieve this by subclassing UIButton:
class ActionButton: UIButton {
var touchDown: ((button: UIButton) -> ())?
var touchExit: ((button: UIButton) -> ())?
var touchUp: ((button: UIButton) -> ())?
required init?(coder aDecoder: NSCoder) { fatalError("init(coder:)") }
override init(frame: CGRect) {
super.init(frame: frame)
setupButton()
}
func setupButton() {
//this is my most common setup, but you can customize to your liking
addTarget(self, action: #selector(touchDown(_:)), forControlEvents: [.TouchDown, .TouchDragEnter])
addTarget(self, action: #selector(touchExit(_:)), forControlEvents: [.TouchCancel, .TouchDragExit])
addTarget(self, action: #selector(touchUp(_:)), forControlEvents: [.TouchUpInside])
}
//actions
func touchDown(sender: UIButton) {
touchDown?(button: sender)
}
func touchExit(sender: UIButton) {
touchExit?(button: sender)
}
func touchUp(sender: UIButton) {
touchUp?(button: sender)
}
}
Use:
let button = ActionButton(frame: buttonRect)
button.touchDown = { button in
print("Touch Down")
}
button.touchExit = { button in
print("Touch Exit")
}
button.touchUp = { button in
print("Touch Up")
}
回答4:
Similar solution to those already listed, but perhaps lighter weight and doesn't rely on randomness to generate unique ids:
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(ObjectIdentifier(self).hashValue) + String(controlEvents.rawValue), sleeve,
objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
}
}
Usage:
button.add(for: .touchUpInside) {
print("Hello, Closure!")
}
回答5:
This is basically Armanoide's answer, above, but with a couple slight changes that are useful for me:
- the passed-in closure can take a
UIButton
argument, allowing you to pass inself
the functions and arguments are renamed in a way that, for me, clarifies what's going on, for instance by distinguishing a Swift closure from a
UIButton
action.private func setOrTriggerClosure(closure:((button:UIButton) -> Void)? = nil) { //struct to keep track of current closure struct __ { static var closure :((button:UIButton) -> Void)? } //if closure has been passed in, set the struct to use it if closure != nil { __.closure = closure } else { //otherwise trigger the closure __. closure?(button: self) } } @objc private func triggerActionClosure() { self.setOrTriggerClosure() } func setActionTo(closure:(UIButton) -> Void, forEvents :UIControlEvents) { self.setOrTriggerClosure(closure) self.addTarget(self, action: #selector(UIButton.triggerActionClosure), forControlEvents: forEvents) }
Much props to Armanoide though for some heavy-duty magic here.
回答6:
I have started to use Armanoide's answer disregarding the fact that it'll be overridden by the second assignment, mainly because at first I needed it somewhere specific which it didn't matter much. But it started to fall apart.
I've came up with a new implementation using AssicatedObjects which doesn't have this limitation, I think has a smarter syntax, but it's not a complete solution:
Here it is:
typealias ButtonAction = () -> Void
fileprivate struct AssociatedKeys {
static var touchUp = "touchUp"
}
fileprivate class ClosureWrapper {
var closure: ButtonAction?
init(_ closure: ButtonAction?) {
self.closure = closure
}
}
extension UIControl {
@objc private func performTouchUp() {
guard let action = touchUp else {
return
}
action()
}
var touchUp: ButtonAction? {
get {
let closure = objc_getAssociatedObject(self, &AssociatedKeys.touchUp)
guard let action = closure as? ClosureWrapper else{
return nil
}
return action.closure
}
set {
if let action = newValue {
let closure = ClosureWrapper(action)
objc_setAssociatedObject(
self,
&AssociatedKeys.touchUp,
closure as ClosureWrapper,
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
)
self.addTarget(self, action: #selector(performTouchUp), for: .touchUpInside)
} else {
self.removeTarget(self, action: #selector(performTouchUp), for: .touchUpInside)
}
}
}
}
As you can see, I've decided to make a dedicated case for touchUpInside
. I know controls have more events than this one, but who are we kidding? do we need actions for every one of them?! It's much simpler this way.
Usage example:
okBtn.touchUp = {
print("OK")
}
In any case, if you want to extend this answer you can either make a Set
of actions for all the event types, or add more event's properties for other events, it's relatively straightforward.
Cheers, M.
回答7:
Swift
After trying all the solutions, this one worked for me for all cases, even when the button in reusable table view cell
import UIKit
typealias UIButtonTargetClosure = UIButton -> ()
class ClosureWrapper: NSObject {
let closure: UIButtonTargetClosure
init(_ closure: UIButtonTargetClosure) {
self.closure = closure
}
}
extension UIButton {
private struct AssociatedKeys {
static var targetClosure = "targetClosure"
}
private var targetClosure: UIButtonTargetClosure? {
get {
guard let closureWrapper = objc_getAssociatedObject(self, &AssociatedKeys.targetClosure) as? ClosureWrapper else { return nil }
return closureWrapper.closure
}
set(newValue) {
guard let newValue = newValue else { return }
objc_setAssociatedObject(self, &AssociatedKeys.targetClosure, ClosureWrapper(newValue), objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}
func addTargetClosure(closure: UIButtonTargetClosure) {
targetClosure = closure
addTarget(self, action: #selector(UIButton.closureAction), forControlEvents: .TouchUpInside)
}
func closureAction() {
guard let targetClosure = targetClosure else { return }
targetClosure(self)
}
}
And then you call it like this:
loginButton.addTargetClosure { _ in
// login logics
}
Resource: https://medium.com/@jackywangdeveloper/swift-the-right-way-to-add-target-in-uibutton-in-using-closures-877557ed9455
回答8:
My solution.
typealias UIAction = () -> Void;
class Button: UIButton {
public var touchUp :UIAction? {
didSet {
self.setup()
}
}
func setup() -> Void {
self.addTarget(self, action: #selector(touchInside), for: .touchUpInside)
}
@objc private func touchInside() -> Void {
self.touchUp!()
}
}
回答9:
Swift 4.2 for UIControl and UIGestureRecognizer, and and remove targets through swift extension stored property paradigm.
Wrapper class for the selector
class Target {
private let t: () -> ()
init(target t: @escaping () -> ()) { self.t = t }
@objc private func s() { t() }
public var action: Selector {
return #selector(s)
}
}
Protocols with associatedtype
s so we can hide hide the objc_
code
protocol PropertyProvider {
associatedtype PropertyType: Any
static var property: PropertyType { get set }
}
protocol ExtensionPropertyStorable: class {
associatedtype Property: PropertyProvider
}
Extension to make the property default and available
extension ExtensionPropertyStorable {
typealias Storable = Property.PropertyType
var property: Storable {
get { return objc_getAssociatedObject(self, String(describing: type(of: Storable.self))) as? Storable ?? Property.property }
set { return objc_setAssociatedObject(self, String(describing: type(of: Storable.self)), newValue, .OBJC_ASSOCIATION_RETAIN) }
}
}
Let us apply the magic
extension UIControl: ExtensionPropertyStorable {
class Property: PropertyProvider {
static var property = [String: Target]()
}
func addTarget(for controlEvent: UIControl.Event = .touchUpInside, target: @escaping () ->()) {
let key = String(describing: controlEvent)
let target = Target(target: target)
addTarget(target, action: target.action, for: controlEvent)
property[key] = target
}
func removeTarget(for controlEvent: UIControl.Event = .touchUpInside) {
let key = String(describing: controlEvent)
let target = property[key]
removeTarget(target, action: target?.action, for: controlEvent)
property[key] = nil
}
}
And to the gestures
extension UIGestureRecognizer: ExtensionPropertyStorable {
class Property: PropertyProvider {
static var property: Target?
}
func addTarget(target: @escaping () -> ()) {
let target = Target(target: target)
addTarget(target, action: target.action)
property = target
}
func removeTarget() {
let target = property
removeTarget(target, action: target?.action)
property = nil
}
}
Example usage:
button.addTarget {
print("touch up inside")
}
button.addTarget { [weak self] in
print("this will only happen once")
self?.button.removeTarget()
}
button.addTarget(for: .touchDown) {
print("touch down")
}
slider.addTarget(for: .valueChanged) {
print("value changed")
}
textView.addTarget(for: .allEditingEvents) { [weak self] in
self?.editingEvent()
}
gesture.addTarget { [weak self] in
self?.gestureEvent()
self?.otherGestureEvent()
self?.gesture.removeTarget()
}
回答10:
I put together a little extension for UIControl that will let you use closures for any action on any UIControl really easily.
You can find it here: https://gist.github.com/nathan-fiscaletti/8308f00ff364b72b6a6dec57c4b13d82
Here are some examples of it in practice:
Setting a Button Action
myButton.action(.touchUpInside, { (sender: UIControl) in
// do something
})
Detecting a Switch changing Values
mySwitch.action(.valueChanged, { (sender: UIControl) in
print("Switch State:", mySwitch.isOn)
})
回答11:
One more optimisation (useful if you use it in many places and don't want to duplicate call to objc_setAssociatedObject
). It allows us to not worry about a dirty part of objc_setAssociatedObject
and keeps it inside ClosureSleeve
's constructor:
class ClosureSleeve {
let closure: () -> Void
init(
for object: AnyObject,
_ closure: @escaping () -> Void
) {
self.closure = closure
objc_setAssociatedObject(
object,
String(format: "[%d]", arc4random()),
self,
objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN
)
}
@objc func invoke () {
closure()
}
}
So your extension will look a tiny bit cleaner:
extension UIControl {
func add(
for controlEvents: UIControlEvents,
_ closure: @escaping ()->()
) {
let sleeve = ClosureSleeve(
for: self,
closure
)
addTarget(
sleeve,
action: #selector(ClosureSleeve.invoke),
for: controlEvents
)
}
}
回答12:
class ViewController : UIViewController {
var aButton: UIButton!
var assignedClosure: (() -> Void)? = nil
override func loadView() {
let view = UIView()
view.backgroundColor = .white
aButton = UIButton()
aButton.frame = CGRect(x: 95, y: 200, width: 200, height: 20)
aButton.backgroundColor = UIColor.red
aButton.addTarget(self, action: .buttonTapped, for: .touchUpInside)
view.addSubview(aButton)
self.view = view
}
func fizzleButtonOn(events: UIControlEvents, with: @escaping (() -> Void)) {
assignedClosure = with
aButton.removeTarget(self, action: .buttonTapped, for: .allEvents)
aButton.addTarget(self, action: .buttonTapped, for: events)
}
@objc func buttonTapped() {
guard let closure = assignedClosure else {
debugPrint("original tap")
return
}
closure()
}
}
fileprivate extension Selector {
static let buttonTapped = #selector(ViewController.buttonTapped)
}
Then at some point in your app's lifecycle, you'll mutate the instances' closure. Here's an example
fizzleButtonOn(events: .touchUpInside, with: { debugPrint("a new tap action") })
回答13:
@Armanoide solution is cool cause it uses trick with struct
and static var
inside it but it is not perfect if you're reusing one button a few times cause in this case action closure will always store the last handler.
I've fixed it for UIKitPlus library
import UIKit
extension UIControl {
private func actionHandler(action: (() -> Void)? = nil) {
struct Storage { static var actions: [Int: (() -> Void)] = [:] }
if let action = action {
Storage.actions[hashValue] = action
} else {
Storage.actions[hashValue]?()
}
}
@objc func triggerActionHandler() {
actionHandler()
}
func actionHandler(controlEvents control: UIControl.Event, forAction action: @escaping () -> Void) {
actionHandler(action: action)
addTarget(self, action: #selector(triggerActionHandler), for: control)
}
}
来源:https://stackoverflow.com/questions/25919472/adding-a-closure-as-target-to-a-uibutton