OgreSwamp
OgreSwamp

Reputation: 4692

Calling selector from protocol extension

I'm building simple theme engine and would like have an extension which adds UISwipeGestureRecognizer to UIViewController

Here is my code:

protocol Themeable {
    func themeDidUpdate(currentTheme: Theme) -> Void
}

extension Themeable where Self: UIViewController {
    func switchCurrentTheme() {
        Theme.switchTheme()
        themeDidUpdate(Theme.currentTheme)
    }

    func addSwitchThemeGestureRecognizer() {
        let gestureRecognizer = UISwipeGestureRecognizer(target: self, action:#selector(Self.switchCurrentTheme))
        gestureRecognizer.direction = .Down
        gestureRecognizer.numberOfTouchesRequired = 2
        self.view.addGestureRecognizer(gestureRecognizer)
    }
}

Of course compiler can't find #selector(Self.switchCurrentTheme) as it isn't exposed via @objc directive. Is it possible to add this behaviour to my extension?

UPDATE: Theme is a Swift enum, so I can't add @objc in front of Themeable protocol

Upvotes: 13

Views: 4248

Answers (4)

Abhijeet Rai
Abhijeet Rai

Reputation: 129

Here is a similar use-case, you can call a method through a selector without using @objc as in swift by using the dynamic keyword. By doing so, you are instructing the compiler to use dynamic dispatch implicitly.

import UIKit

protocol Refreshable: class {

    dynamic func refreshTableData()

    var tableView: UITableView! {get set}
}

extension Refreshable where Self: UIViewController {

    func addRefreshControl() {
        tableView.insertSubview(refreshControl, at: 0)
    }

    var refreshControl: UIRefreshControl {
        get {
            let tmpAddress = String(format: "%p", unsafeBitCast(self, to: Int.self))
            if let control = _refreshControl[tmpAddress] as? UIRefreshControl {
                return control
            } else {
                let control = UIRefreshControl()
                control.addTarget(self, action: Selector(("refreshTableData")), for: .valueChanged)
                _refreshControl[tmpAddress] = control
                return control
            }
        }
    }
}

fileprivate var _refreshControl = [String: AnyObject]()

class ViewController: UIViewController: Refreshable {
    @IBOutlet weak var tableView: UITableView! {
        didSet {
            addRefreshControl()
        }
    }

    func refreshTableData() {
        // Perform some stuff
    }
}

Upvotes: 0

Dan Loewenherz
Dan Loewenherz

Reputation: 11236

The cleanest, working solution I could come up with was to define a private extension on UIViewController with the method in question. By limiting the scope to private, access to this method is isolated to within the source file where the protocol is defined in. Here's what it looks like:

protocol Themeable {
    func themeDidUpdate(currentTheme: Theme) -> Void
}

fileprivate extension UIViewController {
    @objc func switchCurrentTheme() {
        guard let themeableSelf = self as? Themeable else {
            return
        }

        Theme.switchTheme()
        themeableSelf.themeDidUpdate(Theme.currentTheme)
    }
}

extension Themeable where Self: UIViewController {
    func addSwitchThemeGestureRecognizer() {
        let gestureRecognizer = UISwipeGestureRecognizer(target: self, action:#selector(switchCurrentTheme))
        gestureRecognizer.direction = .Down
        gestureRecognizer.numberOfTouchesRequired = 2
        self.view.addGestureRecognizer(gestureRecognizer)
    }
}

Upvotes: 22

OgreSwamp
OgreSwamp

Reputation: 4692

I found a solution. May be not the perfect one, but it works. As I can't define Themeable protocol as @objc because it uses Swift-only enum I decided to move method I want to call to "parent" protocol and define this protocol as @objc. It seems like it works but I don't really like it to be honest...

@objc protocol ThemeSwitcher {
    func switchCurrentTheme()
}

protocol Themeable: ThemeSwitcher {
    func themeDidUpdate(currentTheme: Theme) -> Void
}

extension Themeable where Self: UIViewController {
    func switchCurrentTheme() {
        Theme.switchTheme()
        themeDidUpdate(Theme.currentTheme)
    }

    func addSwitchThemeGestureRecognizer() {
        let gestureRecognizer = UISwipeGestureRecognizer(target: self, action:#selector(switchCurrentTheme))
        gestureRecognizer.direction = .Down
        gestureRecognizer.numberOfTouchesRequired = 2
        self.view.addGestureRecognizer(gestureRecognizer)
    }
}

Upvotes: 1

Clafou
Clafou

Reputation: 15400

Have you considered creating a wrapper to let you call your non-@objc function from an @objc one?

@objc class Wrapper: NSObject {
    let themeable: Themeable

    init(themeable: Themeable) {
        self.themeable = themeable
    }

    func switchCurrentTheme() {
        Theme.switchTheme()
        themeable.themeDidUpdate(Theme.currentTheme)
    }
}

protocol Themeable {
    func themeDidUpdate(currentTheme: Theme) -> Void
}

extension Themeable where Self: UIViewController {
    func addSwitchThemeGestureRecognizer() {
        let wrapper = Wrapper(themeable: self)
        let gestureRecognizer = UISwipeGestureRecognizer(target: wrapper, action:#selector(Wrapper.switchCurrentTheme))
        gestureRecognizer.direction = .Down
        gestureRecognizer.numberOfTouchesRequired = 2
        self.view.addGestureRecognizer(gestureRecognizer)
    }
}

Upvotes: 0

Related Questions