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

2019-01-18 10:31发布

问题:

I want to make a selector argument of my method refer to a closure property, both of them exist in the same scope. For example,

func backgroundChange() {
    self.view.backgroundColor = UIColor.blackColor()
    self.view.alpha = 0.55

    let backToOriginalBackground = {
        self.view.backgroundColor = UIColor.whiteColor()
        self.view.alpha = 1.0
    }

    NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: #selector(backToOriginalBackground), userInfo: nil, repeats: false)
}

However, this shows an error: Argument of #selector cannot refer to a property.

Of course I can define a new, separate method and move the implementation of the closure to it, but I want to keep it frugal for such a small implementation.

Is it possible to set a closure to #selector argument?

回答1:

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)


回答2:

Not directly, but some workarounds are possible. Take a look at the following example.

/// Target-Action helper.
final class Action: NSObject {

    private let _action: () -> ()

    init(action: () -> ()) {
        _action = action
        super.init()
    }

    func action() {
        _action()
    }

}

let action1 = Action { print("action1 triggered") }

let button = UIButton()
button.addTarget(action1, action: #selector(action1.action), forControlEvents: .TouchUpInside)


回答3:

I tried this for UIBarButtonItem at least:

private var actionKey: Void?

extension UIBarButtonItem {

    private var _action: () -> () {
        get {
            return objc_getAssociatedObject(self, &actionKey) as! () -> ()
        }
        set {
            objc_setAssociatedObject(self, &actionKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    convenience init(title: String?, style: UIBarButtonItemStyle, action: @escaping () -> ()) {
        self.init(title: title, style: style, target: nil, action: #selector(pressed))
        self.target = self
        self._action = action
    }

    @objc private func pressed(sender: UIBarButtonItem) {
        _action()
    }

}

Then you can do this:

navigationItem.leftBarButtonItem = UIBarButtonItem(title: "Test", style: .plain, action: {
    print("Hello World!")
})


回答4:

It is now possible. I've created a gist for block-based selectors in Swift 4.

https://gist.github.com/cprovatas/98ff940140c8744c4d1f3bcce7ba4543

Usage:

UIButton().addTarget(Selector, action: Selector { debugPrint("my code here") }, for: .touchUpInside)



回答5:

No, #selector refers to an Objective-C method.

You can do something much better though: Add an extension to NSTimer that lets you create a scheduled timer not with a target and selector, but with a closure.



回答6:

You can use ActionClosurable which support UIControl, UIButton, UIRefreshControl, UIGestureRecognizer and UIBarButtonItem. https://github.com/takasek/ActionClosurable

Bellow show example of UIBarButtonItem

// UIBarButtonItem
let barButtonItem = UIBarButtonItem(title: "title", style: .plain) { _ in
    print("barButtonItem title")
}


回答7:

If you change the scope of block to a class scope rather than function and hold a reference to closure there.

You could invoke that closure with a function. in the class. So that way you can invoke that closure as a selector.

Something like this:

class Test: NSObject {
    let backToOriginalBackground = {

    }
    func backgroundChange() {
        NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: #selector(test), userInfo: nil, repeats: false)
    }

    func test() {
        self.backToOriginalBackground()
    }
}


回答8:

My solution was to create a class block variable like:

let completionBlock: () -> () = nil

Create a method which calls this completionBlock:

func completed(){
    self.completionBlock!()
}

And inside where I want to put my selector like a block I did:

func myFunc(){
  self.completionBlock = {//what I want to be done}
  NSTimer.scheduledTimerWithTimeInterval(0.5, target: self, selector: #selector(Myclass.completed), userInfo: nil, repeats: false)
}


回答9:

So my answer to having a selector be assigned to a closure in a swift like manner is similar to some of the answers already, but I thought I would share a real life example of how I did it within a UIViewController extension.

fileprivate class BarButtonItem: UIBarButtonItem {
  var actionCallback: ( () -> Void )?
  func buttonAction() {
    actionCallback?()
  }
}

fileprivate extension Selector {
  static let onBarButtonAction = #selector(BarButtonItem.buttonAction)
}

extension UIViewController {
  func createBarButtonItem(title: String, action: @escaping () -> Void ) -> UIBarButtonItem {
    let button = BarButtonItem(title: title, style: .plain, target nil, action: nil)
    button.actionCallback = action
    button.action = .onBarButtonAction
    return button
  }
}

// Example where button is inside a method of a UIViewController 
// and added to the navigationItem of the UINavigationController

let button = createBarButtonItem(title: "Done"){
  print("Do something when done")
}

navigationItem.setLeftbarButtonItems([button], animated: false)