Search code examples
iosswiftuikitclosuresuiswitch

Is it possible in Swift to make a closure that is exposed to Objective-C? And is it worth it for making targets for interface elements?


To provide more context: a screen in my app has a table, some table cells have switches in them (UISwitch).

I want to give each switch a target the moment it is created, like so:

cell.switch.addTarget(self, action: #selector({// do stuff }), for: .valueChanged)

And I want the function to be made into a selector to be inline, since it should be slightly different (changing a different value in an array) depending on the number of the row in which the switch has been flipped.

The problem is - the block above does not compile because the closure is not exposed to objective-C. Can i somehow expose it in the same line?

I do know about the option to tag my switches and make a shared called function for them, which behaves differently depending on the tag, like this:

cell.switch.tag = switchNumber
cell.switch.addTarget(self, action: #selector(switchFlipped), for: .valueChanged)
@objc func switchFlipped(sender: UISwitch){
        let switchNumber = sender.tag
        switch(tag){
            // do stuff depending on the case
        }
    }

But I feel like possibly making an inline closure is quicker. Am I wrong?


Solution

  • In short, no, you can't pass a Swift closure to the addTarget/selector API from UIControl.

    What you should be doing is hiding all of these details in the implementation of your custom table view cell class. Your cellForRowAt data source method should not even know that the cell has a switch.

    Your custom cell class should make itself the handler of the switch's valueChanged action. Your custom cell class should provide a callback closure property that it calls when the switch's value changes. In your cellForRowAt you can assign a closure to this property.

    This setup keeps the details of the cell's features where they belong - in the cell class. This also makes your implementation of cellForRowAt much cleaner and it allows you to use a closure to handle whatever events your custom cell may trigger.

    Here's a rough example for the custom cell class:

    class MyCustomCell: UITableViewCell {
        var actionHandler: ((MyCustomCell, Bool) -> Void)?
    
        private var aSwitch: UISwitch!
    
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: style, reuseIdentifier: reuseIdentifier)
    
            commonInit()
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
    
            commonInit()
        }
    
        private func commonInit() {
            // setup switch in storyboard or here in code as needed
    
            aSwitch.addTarget(self, action: #selector(switchChanged), for: .valueChanged)
        }
    
        @objc func switchChanged(_ sender: UISwitch) {
            actionHandler?(self, sender.isOn)
        }
    }
    

    And your cellForRowAt:

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "someIdentifier", for: indexPath) as! MyCustomCell
    
        cell.actionHandler = { (cell, state) in
            // Do stuff
        }
    
        return cell
    }
    

    Adjust the details as needed for your setup.