Protocol extension on an ObjC protocol
Asked Answered
P

2

25

I have an Objective-C protocol which is used by mostly objective-C objects and one or two Swift objects.

I would like to extend the protocol in Swift and add 2 functions. One to register for a notification and another to handle the notification.

If I add these

func registerForPresetLoadedNotification() {
    NSNotificationCenter.defaultCenter().addObserver(self as AnyObject,
                                                     selector: #selector(presetLoaded(_:)),
                                                     name: kPresetLoadedNotificationName,
                                                     object: nil)
}

func presetLoaded(notification: NSNotification) {
    
}

I get an error on the #selector which says:

Argument of '#selector' refers to a method that is not exposed to Objective-C

If I then mark presetLoaded as @objc I get an error which says:

@objc can only be used with members of classes, @objc protocols, and concrete extensions of classes

I also cannot mark the protocol extension as @objc

When I create the Objective-C protocol as a Swift protocol I get the same error.

Is there a way to achieve this that will work for Objective-C and Swift classes that use the protocol?

Peart answered 16/8, 2016 at 17:19 Comment(4)
How about extending the protocol and defining these functions as optional functions in Objective-C and implementing them in Swift? Just a thought.Maimaia
My idea was to implement the register function in the extension and then override the notification handler function in the objective-c and swift classes. I need to set parameters in the objective-c classes, implementing them in Swift wont work.Peart
I'm not sure I understand you correctly. It sounds like what you want to do is extend (add functions to existing protocol) and provide a default implementation in ObjC and override a part of that implementation in Swift. Is this correct?Maimaia
Other way around, default implementation in the swift protocol extension and override the function in objective-c and swift classes.Peart
G
9

Indeed, you can't really mark a function of a protocol extension as @objc (or dynamic, which is equivalent by the way). Only methods of a class are allowed to be dispatched by Objective-C runtime.

In your particular case, if you really want to make it through protocol extension, I can propose the following solution (assuming your original protocol is named ObjcProtocol).

Let's make a wrapper for our notification handler:

final class InternalNotificationHandler {
    private let source: ObjcProtocol

    init(source: ObjcProtocol) {
        // We require source object in case we need access some properties etc.
        self.source = source
    }

    @objc func presetLoaded(notification: NSNotification) {
        // Your notification logic here
    }
}

Now we need extend our ObjcProtocol to introduce required logic

import Foundation
import ObjectiveC

internal var NotificationAssociatedObjectHandle: UInt8 = 0

extension ObjcProtocol {
    // This stored variable represent a "singleton" concept
    // But since protocol extension can only have stored properties we save it via Objective-C runtime
    private var notificationHandler: InternalNotificationHandler {
        // Try to an get associated instance of our handler
        guard let associatedObj = objc_getAssociatedObject(self, &NotificationAssociatedObjectHandle)
            as? InternalNotificationHandler else {
            // If we do not have any associated create and store it
            let newAssociatedObj = InternalNotificationHandler(source: self)
            objc_setAssociatedObject(self,
                                     &NotificationAssociatedObjectHandle,
                                     newAssociatedObj,
                                     objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
            return newAssociatedObj
        }

        return associatedObj
    }

    func registerForPresetLoadedNotification() {
        NSNotificationCenter.defaultCenter().addObserver(self,
                                                         selector: #selector(notificationHandler.presetLoaded(_:)),
                                                         name: kPresetLoadedNotificationName,
                                                         object: self)
    }

    func unregisterForPresetLoadedNotification() {
        // Clear notification observer and associated objects
        NSNotificationCenter.defaultCenter().removeObserver(self,
                                                            name: kPresetLoadedNotificationName,
                                                            object: self)
        objc_removeAssociatedObjects(self)
    }
}

I know this might look not so elegant, so I'd really consider changing a core approach.

One note: You do might want to restrict your protocol extension

extension ObjcProtocol where Self: SomeProtocolOrClass
Giusto answered 16/8, 2016 at 21:8 Comment(0)
G
6

I found a way to do it :) Just avoid @objc all together :D

//Adjusts UITableView content height when keyboard show/hide
public protocol KeyboardObservable: NSObjectProtocol {
    func registerForKeyboardEvents()
    func unregisterForKeyboardEvents()
}

extension KeyboardObservable where Self: UITableView {

    public func registerForKeyboardEvents() {
        let defaultCenter = NotificationCenter.default

        var tokenShow: NSObjectProtocol!
        tokenShow = defaultCenter.addObserver(forName: .UIKeyboardDidShow, object: nil, queue: nil) { [weak self] (notification) in
            guard self != nil else {
                defaultCenter.removeObserver(tokenShow)
                return
            }
            self!.keyboardWilShow(notification as NSNotification)
        }

        var tokenHide: NSObjectProtocol!
        tokenHide = defaultCenter.addObserver(forName: .UIKeyboardWillHide, object: nil, queue: nil) { [weak self] (notification) in
            guard self != nil else {
                defaultCenter.removeObserver(tokenHide)
                return
            }
            self!.keyboardWilHide(notification as NSNotification)
        }
    }

    private func keyboardDidShow(_ notification: Notification) {
        let rect = ((notification as NSNotification).userInfo![UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
        let height = rect.height
        var insets = UIEdgeInsetsMake(0, 0, height, 0)
        insets.top = contentInset.top
        contentInset = insets
        scrollIndicatorInsets = insets
    }

    private func keyboardWillHide(_ notification: Notification) {
        var insets = UIEdgeInsetsMake(0, 0, 0, 0)
        insets.top = contentInset.top
        UIView.animate(withDuration: 0.3) { 
            self.contentInset = insets
            self.scrollIndicatorInsets = insets
        }
    }

    public func unregisterForKeyboardEvents() {
        NotificationCenter.default.removeObserver(self)
    }

}

Example

class CreateStudentTableView: UITableView, KeyboardObservable {

  init(frame: CGRect, style: UITableViewStyle) {
    super.init(frame: frame, style: style)
    registerForKeyboardEvents()
  }

  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}
Gun answered 29/1, 2018 at 11:36 Comment(1)
Block based observers cannot be removed using NotificationCenter.default.removeObserver(self) . You need to use the observer object returned by addObserverForName:object:queue:usingBlock: Refer : developer.apple.com/documentation/foundation/…Merill

© 2022 - 2024 — McMap. All rights reserved.