Calling selector from protocol extension
Asked Answered
D

4

13

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

Demography answered 14/4, 2016 at 16:52 Comment(0)
D
22

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)
    }
}
Downtrend answered 8/7, 2016 at 21:36 Comment(1)
Thanks for this! Worked like a charm. :) In Swift 3, the private extension of UIViewController now needs to be fileprivate instead. Also, the switchCurrentTheme function should be fileprivate instead of internal.Sopor
D
1

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)
    }
}
Demography answered 15/4, 2016 at 10:36 Comment(1)
still doesn't work as I can't apply @objc to my ViewController which implements Themeable protocol..Demography
E
0

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)
    }
}
Esquivel answered 15/4, 2016 at 11:32 Comment(1)
This is a great idea, but couldn't get it to work. :(Downtrend
C
0

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
    }
}
Chura answered 19/12, 2019 at 9:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.