简体   繁体   中英

Swift use selector argument like a closure

I was just wondering if it was possible to pass a function to a button action (which is usually a selector).

For example, normally I'd say:

UIBarButtonItem(title: "Press", style: .Done, target: self, action: "functionToCall")

func functionToCall() {
    // Do something
}

But I was wondering if it's possible to do something like:

UIBarButtonItem(title: "Press", style: .Done, target: self, action: {
    // Do Something
})

Reason I want to do this is because my function is super simple and it seems like it would be neater and more Swift-like what with the emphasis they are placing on closures.

Here's an updated solution for Swift 3.

class BlockBarButtonItem: UIBarButtonItem {
  private var actionHandler: ((Void) -> Void)?

  convenience init(title: String?, style: UIBarButtonItemStyle, actionHandler: ((Void) -> Void)?) {
    self.init(title: title, style: style, target: nil, action: #selector(barButtonItemPressed))
    self.target = self
    self.actionHandler = actionHandler
  }

  convenience init(image: UIImage?, style: UIBarButtonItemStyle, actionHandler: ((Void) -> Void)?) {
    self.init(image: image, style: style, target: nil, action: #selector(barButtonItemPressed))
    self.target = self
    self.actionHandler = actionHandler
  }

  func barButtonItemPressed(sender: UIBarButtonItem) {
    actionHandler?()
  }
}

This is an alternative solution without subclassing:

extension UIBarButtonItem {
    private struct AssociatedObject {
        static var key = "action_closure_key"
    }

    var actionClosure: (()->Void)? {
        get {
            return objc_getAssociatedObject(self, &AssociatedObject.key) as? ()->Void
        }
        set {
            objc_setAssociatedObject(self, &AssociatedObject.key, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            target = self
            action = #selector(didTapButton(sender:))
        }
    }

    @objc func didTapButton(sender: Any) {
        actionClosure?()
    }
}

It relies on the associated objects from the Objective-C runtime to add a closure/block property.

When set, it changes the target to itself and points the selector to a new function that calls the closure if it exists.

With this, in any moment you can just set the actionClosure of any UIBarButtonItem and expect everything to work, here's an example:

let button = UIBarButtonItem(
    barButtonSystemItem: .action,
    target: nil, action: nil
)
button.actionClosure = {
    print("hello")
}

A post on Reddit explains a solution for this with a custom component - https://www.reddit.com/r/swift/comments/3fjzap/creating_button_action_programatically_using

To use it though I had to add the Button programatically rather than through storyboard. Here was how I did it.

let tempVariableIWantToReference = "Classy"

navigationTitle.leftBarButtonItem = BlockBarButtonItem.init(
   title: "< Back", style: UIBarButtonItemStyle.Plain, 
   actionHandler: { () -> Void in
       print("Hey I'm in a closure")
       print("I can even reference temporary variables. \(self.tempVariableIWantToReference)!!!")
   })
  • Updated for Swift 5.
  • Implements all of the convenience initializers that have targets.
  • Adds a typealias for the function signature to clean up the syntax.
  • Passes the UIBarButtonItem into the action handler in keeping with Cocoa conventions.
import UIKit

class SwiftBarButtonItem: UIBarButtonItem {
    typealias ActionHandler = (UIBarButtonItem) -> Void

    private var actionHandler: ActionHandler?

    convenience init(image: UIImage?, style: UIBarButtonItem.Style, actionHandler: ActionHandler?) {
        self.init(image: image, style: style, target: nil, action: #selector(barButtonItemPressed(sender:)))
        target = self
        self.actionHandler = actionHandler
    }

    convenience init(title: String?, style: UIBarButtonItem.Style, actionHandler: ActionHandler?) {
        self.init(title: title, style: style, target: nil, action: #selector(barButtonItemPressed(sender:)))
        target = self
        self.actionHandler = actionHandler
    }

    convenience init(barButtonSystemItem systemItem: UIBarButtonItem.SystemItem, actionHandler: ActionHandler?) {
        self.init(barButtonSystemItem: systemItem, target: nil, action: #selector(barButtonItemPressed(sender:)))
        target = self
        self.actionHandler = actionHandler
    }

    @objc func barButtonItemPressed(sender: UIBarButtonItem) {
        actionHandler?(sender)
    }
}

Unfortunatelly it is not possible with Apple provided initialisers. The way this works in the background is with reflection, and providing a closure is something completely different which isn't currently supported.

You might be able to create a custom solution with some hacking or Apple may introduce that in the future.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM