Swift make protocol extension a Notification observer
Asked Answered
Q

5

27

Let's consider the following code:

protocol A {
    func doA()
}

extension A {
  func registerForNotification() {
      NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardDidShow:"), name: UIKeyboardDidShowNotification, object: nil)
  }

  func keyboardDidShow(notification: NSNotification) {

  }
}

Now look at a UIViewController subclass that implements A:

class AController: UIViewController, A {
   override func viewDidLoad() {
      super.viewDidLoad()
      self.registerForNotification()
      triggerKeyboard()
   }

   func triggerKeyboard() {
      // Some code that make key board appear
   }

   func doA() {
   }
}

But surprisingly this crashes with an error:

keyboardDidShow:]: unrecognized selector sent to instance 0x7fc97adc3c60

So should I implement the observer in the view controller itself? Can't it stay in the extension?

Following things already tried.

making A a class protocol. Adding keyboardDidShow to protocol itself as signature.

protocol A:class {
   func doA()
   func keyboardDidShow(notification: NSNotification)
}
Quoin answered 8/10, 2015 at 17:40 Comment(7)
I've tried something similar too in the past, but I found out that Swift's protocol extensions don't work with Objective-C protocols and classes, but apparently they somehow do, I'm confusedShellfish
extension A{} ??? Are you talking about extension Controller{}Aphorize
You just need to add the parameter to the method or delete : from the end of the selector's nameFavianus
@MidhunMP . Yes it is extension A{}. New feature in Swift 2 onwards. Which is called protocol extensions. Which enables even adding default functionality to protocol methods.Quoin
The method is func keyboardDidShow(notification: NSNotification) which makes a match with Selector("keyboardDidShow:")Quoin
did you file a bug or feature request for this?Lissome
got the same problem,does anyone have a solution ?Fusiform
L
36

I solved a similar problem by implementing the newer - addObserverForName:object:queue:usingBlock: method of NSNotificationCenter and calling the method directly.

extension A where Self: UIViewController  {
    func registerForNotification() {
        NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardDidShowNotification, object: nil, queue: nil) { [unowned self] notification in
            self.keyboardDidShow(notification)
        }
    }

    func keyboardDidShow(notification: NSNotification) {
        print("This will get called in protocol extension.")
    }
}

This example will cause keyboardDidShow to be called in the protocol extension.

Lordan answered 19/1, 2016 at 3:56 Comment(9)
so obvious, but didn't think of it - cudos!Devilment
Great find! Missed that one :)Chantellechanter
How do you remove the observer? let's say I want to have a function that removes the observer (in the extension as well) and then just call that from deinitHammond
@Hammond You could also add a func unregisterForNotification()Lordan
In my case I needed to use weak, not unownedDetribalize
@MarkLeonard does that allow the view controller to deallocate?Lordan
@JamesPaolantonio can you be more specific on how you would implement an unregisterForNotification method? To remove the observer, you need to store the return of the addObserverForName, but that is not possible on an extensionIerna
Posted an answer that also removes the observer in a unregisterForNotification functionBookmark
Using unowned here will crash the app if the notification fires after the controller has deallocated! That should be a weak self and definitely not unowned.Cubbyhole
B
3

In addition to James Paolantonio's answer. A unregisterForNotification method can be implemented using associated objects.

var pointer: UInt8 = 0

extension NSObject {
    var userInfo: [String: Any] {
        get {
            if let userInfo = objc_getAssociatedObject(self, &pointer) as? [String: Any] {
                return userInfo
            }
            self.userInfo = [String: Any]()
            return self.userInfo
        }
        set(newValue) {
            objc_setAssociatedObject(self, &pointer, newValue, .OBJC_ASSOCIATION_RETAIN)
        }
    }
}

protocol A {}
extension A where Self: UIViewController {

    var defaults: NotificationCenter {
        get {
            return NotificationCenter.default
        }
    }

    func keyboardDidShow(notification: Notification) {
        // Keyboard did show
    }

    func registerForNotification() {
        userInfo["didShowObserver"] = defaults.addObserver(forName: .UIKeyboardDidShow, object: nil, queue: nil, using: keyboardDidShow)
    }

    func unregisterForNotification() {
        if let didShowObserver = userInfo["didShowObserver"] as? NSObjectProtocol {
            defaults.removeObserver(didShowObserver, name: .UIKeyboardDidShow, object: nil)
        }
    }
}
Bookmark answered 10/3, 2017 at 11:47 Comment(0)
B
1

To avoid the crash, implement the observer method in the Swift class that uses the protocol.

The implementation has to be in the Swift class itself, not just the protocol extension, because a selector always refers to an Objective-C method, and a function within a protocol extension is not available as an Objective-C selector. Yet methods from a Swift class are available as Objective-C selectors if the Swift class inherits from an Objective-C class

“If your Swift class inherits from an Objective-C class, all of the methods and properties in the class are available as Objective-C selectors.”

Also, in Xcode 7.1, self has to be downcast to AnyObject when specifying it as the observer in the addObserver call.

protocol A {
    func doA()
}

extension A {
    func registerForNotification() {
        NSNotificationCenter.defaultCenter().addObserver(self as! AnyObject,
            selector: Selector("keyboardDidShow:"),
            name: UIKeyboardDidShowNotification,
            object: nil)
    }

    func keyboardDidShow(notification: NSNotification) {
        print("will not appear")
    }
}

class ViewController: UIViewController, A {
    override func viewDidLoad() {
        super.viewDidLoad()
        self.registerForNotification()
        triggerKeyboard()
    }

    func triggerKeyboard(){
        // Some code that makes the keyboard appear
    }

    func doA(){
    }

    func keyboardDidShow(notification: NSNotification) {
        print("got the notification in the class")
    }
}
Bagging answered 24/10, 2015 at 23:0 Comment(0)
P
1

Using selectors in Swift requires that your concrete class must inherit from NSObject. To enforce this in a protocol extension, you should use where. For example:

protocol A {
    func doA()
}

extension A where Self: NSObject {
  func registerForNotification() {
      NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardDidShow:"), name: UIKeyboardDidShowNotification, object: nil)
  }

  func keyboardDidShow(notification: NSNotification) {

  }
}
Pannonia answered 19/1, 2016 at 4:54 Comment(0)
B
0

I solved it using NSObjectProtocol as below,

@objc protocol KeyboardNotificaitonDelegate: NSObjectProtocol {
func keyboardWillBeShown(notification: NSNotification)
func keyboardWillBeHidden(notification: NSNotification)
}

extension KeyboardNotificaitonDelegate {

func registerForKeyboardNotifications() {
    //Adding notifies on keyboard appearing
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillBeShown(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
    NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillBeHidden(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}

func deregisterFromKeyboardNotifications() {
    //Removing notifies on keyboard appearing
    NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillShow, object: nil)
    NotificationCenter.default.removeObserver(self, name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
}
Beutler answered 14/8, 2017 at 5:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.