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

22,593

Solution 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)

Solution 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: @escaping () -> ()) {
        _action = action
        super.init()
    }

    @objc func action() {
        _action()
    }

}

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

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

Solution 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!")
})

Solution 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)`

Solution 5

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")
}
Share:
22,593
Blaszard
Author by

Blaszard

I'm here to gain knowledge and insights on a variety of fields I'm interested in. Specifically, Programming & Software Development (Python and R; no longer use Swift and JavaScript/node.js) Data Science, Machine Learning, AI, & statistics Travel (started in 2016) Language (普通话, français, español, italiano, русский, 한국어) Politics, Economics, and Finance Currently (in 2020), my primary interest is Korean and Russian😈 PS: I'm not a native-English speaker. If you find any errors in my grammar and expressions, don't hesitate to edit it. I'll appreciate it👨🏻‍💼

Updated on July 09, 2022

Comments

  • Blaszard
    Blaszard almost 2 years

    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?

  • ryantxr
    ryantxr about 8 years
    I would be interested in seeing what that looks like. That would be useful in so many ways.
  • Prajeet Shrestha
    Prajeet Shrestha about 8 years
    Yeah! That's it. And we were pondering with the syntax rather than the problem OP was facing and it's better solution.
  • Sulthan
    Sulthan about 8 years
    Creating that target is a tiny bit tricky because NSTimer retains its target but definitely possible. Better to use dispatch_after though.
  • Charlton Provatas
    Charlton Provatas over 6 years
    Please see my post below. It is possible w/ some objc-runtime magic :)
  • andrewmclean
    andrewmclean over 6 years
    This is very handy and nice to keep as a Swift implementation. Thank you
  • hexaflexagonal
    hexaflexagonal about 5 years
    This is slick! Wish they'd add a real way to do it to Cocoa, but this is the least hacky workaround I've seen.
  • timeSmith
    timeSmith almost 5 years
    Note: you have to keep an instance of the action that will be used by a button; the button does not take ownership of it.
  • Charlton Provatas
    Charlton Provatas over 4 years
    You have to import the obj-c class for this to work
  • clearlight
    clearlight about 2 years
    Cool! But what a hassle. It needs to be integrated into iOS platform natively and cleanly to be practical in most cases IMO. In my case I wanted to move a couple of @objc selector-target functions from class scope to become sub-functions of a function, bringing them closer in file/scope to 1 place they were invoked, but xcode said #selector can't call local functions... so wanted a way to let a selector run a closure... but this is wayyy too awkward a solution for a minor issue like that. As of this writing WWDC 2022 is just 2wks away... who knows? Maybe some sympathetic Apple developer...?
  • clearlight
    clearlight about 2 years
    Cat / hamburger. Nice example. u a riot