I have what I thought to be a very simple protocol extension for my UIViewController
s providing the capability to dismiss a keyboard through a tap gesture. Here
As Matt said, you can't implement @objc
methods in a protocol. Frédéric's answer covers Notifications
, but what can you do about standard Selectors
?
Let's say you have a protocol & extension, like so
protocol KeyboardHandler {
func setupToolbar()
}
extension KeyboardHandler {
func setupToolbar() {
let toolbar = UIToolbar()
let doneButton = UIBarButtonItem(title: "Done",
style: .done,
target: self,
action: #selector(self.donePressed))
}
@objc func donePressed() {
self.endEditing(true)
}
}
This will generate an error, as we know. What we can do, is take advantage of callbacks.
protocol KeyboardHandler {
func setupToolbar(callback: (_ doneButton: UIBarButtonItem) -> Void))
}
extension KeyboardHandler {
func setupToolbar(callback: (_ doneButton: UIBarButtonItem) -> Void)) {
let toolbar = UIToolbar()
let doneButton = UIBarButtonItem(title: "Done",
style: .done,
target: self,
action: nil
callback(doneButton)
}
}
Then, add an extension for the class you want to implement your protocol
extension ViewController: KeyboardHandler {
func addToolbar(textField: UITextField) {
addToolbar(textField: textField) { doneButton in
doneButton.action = #selector(self.donePressed)
}
}
@objc func donePressed() {
self.view.endEditing(true)
}
}
Instead of setting the action on creation, set it just after creation in the callback.
This way, you still get your desired functionality and can call the function in your class (ex. ViewController
) without even seeing the callbacks!
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
}
}
@Frédéric Adda answer have the downside that you are responsible to unregister your observer, because it uses the block based way of adding an observer. In iOS 9 and later, the 'normal' way of adding an observer, will hold a weak reference to the observer and therefore the developer doesn't have to unregister the observer.
The following way will use the 'normal' way of adding an observer through protocol extensions. It uses a bridging class that will hold the selector.
Pro's:
Con's:
self
is fully initialized.Code:
/// Not really the user info from the notification center, but this is what we want 99% of the cases anyway.
public typealias NotificationCenterUserInfo = [String: Any]
/// The generic object that will be used for sending and retrieving objects through the notification center.
public protocol NotificationCenterUserInfoMapper {
static func mapFrom(userInfo: NotificationCenterUserInfo) -> Self
func map() -> NotificationCenterUserInfo
}
/// The object that will be used to listen for notification center incoming posts.
public protocol NotificationCenterObserver: class {
/// The generic object for sending and retrieving objects through the notification center.
associatedtype T: NotificationCenterUserInfoMapper
/// For type safety, only one notification name is allowed.
/// Best way is to implement this as a let constant.
static var notificationName: Notification.Name { get }
/// The selector executor that will be used as a bridge for Objc - C compability.
var selectorExecutor: NotificationCenterSelectorExecutor! { get set }
/// Required implementing method when the notification did send a message.
func retrieved(observer: T)
}
public extension NotificationCenterObserver {
/// This has to be called exactly once. Best practise: right after 'self' is fully initialized.
func register() {
assert(selectorExecutor == nil, "You called twice the register method. This is illegal.")
selectorExecutor = NotificationCenterSelectorExecutor(execute: retrieved)
NotificationCenter.default.addObserver(selectorExecutor, selector: #selector(selectorExecutor.hit), name: Self.notificationName, object: nil)
}
/// Retrieved non type safe information from the notification center.
/// Making a type safe object from the user info.
func retrieved(userInfo: NotificationCenterUserInfo) {
retrieved(observer: T.mapFrom(userInfo: userInfo))
}
/// Post the observer to the notification center.
func post(observer: T) {
NotificationCenter.default.post(name: Self.notificationName, object: nil, userInfo: observer.map())
}
}
/// Bridge for using Objc - C methods inside a protocol extension.
public class NotificationCenterSelectorExecutor {
/// The method that will be called when the notification center did send a message.
private let execute: ((_ userInfo: NotificationCenterUserInfo) -> ())
public init(execute: @escaping ((_ userInfo: NotificationCenterUserInfo) -> ())) {
self.execute = execute
}
/// The notification did send a message. Forwarding to the protocol method again.
@objc fileprivate func hit(_ notification: Notification) {
execute(notification.userInfo! as! NotificationCenterUserInfo)
}
}
From my GitHub (you can't use the code through Cocoapods): https://github.com/Jasperav/JVGenericNotificationCenter
I have made another attempt, from another point of view. I use in many of my developments, a protocol to handle the style of UINavigationBar
in a global way, from each of the UIViewController
contained in it.
One of the biggest problems of doing this is the standard behavior to return to the previous UIViewController
(pop) and dismiss a UIViewController
shown in a modal way. Let's look at some code:
public protocol NavigationControllerCustomizable {
}
extension NavigationControllerCustomizable where Self: UIViewController {
public func setCustomBackButton(on navigationItem: UINavigationItem) {
let backButton = UIButton()
backButton.setImage(UIImage(named: "navigationBackIcon"), for: .normal)
backButton.tintColor = navigationController?.navigationBar.tintColor
backButton.addTarget(self, action: #selector(defaultPop), for: .touchUpInside)
let barButton = UIBarButtonItem(customView: backButton)
navigationItem.leftBarButtonItem = barButton
}
}
This is a very simplified (and slightly modified) version of the original protocol, although it will be worth explaining the example.
As you can see, a #selector is being set within a protocol extension. As we know, protocol extensions are not exposed to Objective-C and therefore this will generate an error.
My solution is to wrap the methods that handle the standard behaviors of all my UIViewController
(pop and dismiss) in another protocol and extend UIViewController
to it. Viewing this in code:
public protocol NavigationControllerDefaultNavigable {
func defaultDismiss()
func defaultPop()
}
extension UIViewController: NavigationControllerDefaultNavigable {
public func defaultDismiss() {
dismiss(animated: true, completion: nil)
}
public func defaultPop() {
navigationController?.popViewController(animated: true)
}
}
With this workaround, all UIViewController
implementing the NavigationControllerCustomizable
will immediately have the methods defined in NavigationControllerDefaultNavigable
, with their default implementation, and therefore be accessible from Objective-C to create expressions of type #selector, without any type of error.
I hope this explanation can help someone.
This is a Swift protocol extension. Swift protocol extensions are invisible to Objective-C, no matter what; it knows nothing of them. But #selector
is about Objective-C seeing and calling your function. That is not going to happen because your on(tap:)
function is defined only in the protocol extension. Thus the compiler rightly stops you.
This question is one of a large class of questions where people think they are going to be clever with protocol extensions in dealing with Cocoa by trying to inject Objective-C-callable functionality (selector, delegate method, whatever) into a class via a protocol extension. It's an appealing notion but it's just not going to work.
Here's my idea: avoid mixing swift protocol & objc protocol.