Kamil
Kamil

Reputation: 248

Swift NSNotificationCenter observer in class extension crash

I tried to create a universal mechanism that could react to keyboard showing and hiding in iOS. I came up with simple protocol and its extension that doesn't contain real animations but pulls them from object that adopts this protocol:

import UIKit

protocol KeyboardAnimatable {
    func keyboardWillShowAnimation() -> (() -> Void)?
    func keyboardWillHideAnimation() -> (() -> Void)?
}

extension KeyboardAnimatable where Self: UIViewController {

    func setupKeyboardNotifcationListeners() {
        NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name: UIKeyboardWillShowNotification, object: nil)
        NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name: UIKeyboardWillHideNotification, object: nil)
    }

    func removeKeyboardNotificationListeners() {
        NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillShowNotification, object: nil)
        NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: nil)
    }

    func keyboardWillShow(notification: NSNotification) {
        let userInfo = notification.userInfo as! Dictionary<String, AnyObject>
        let animationDuration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSTimeInterval
        let animationCurve = userInfo[UIKeyboardAnimationCurveUserInfoKey]!.intValue
        let curveAnimationOption = UIViewAnimationOptions(rawValue: UInt(animationCurve))
        let options: UIViewAnimationOptions = [.BeginFromCurrentState, curveAnimationOption]

        if let animation = keyboardWillShowAnimation() {
            UIView.animateWithDuration(animationDuration, delay: 0, options: options, animations: animation, completion: nil)
        }
    }

    func keyboardWillHide(notification: NSNotification) {
        let userInfo = notification.userInfo as! Dictionary<String, AnyObject>
        let animationDuration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSTimeInterval
        let animationCurve = userInfo[UIKeyboardAnimationCurveUserInfoKey]!.intValue
        let curveAnimationOption = UIViewAnimationOptions(rawValue: UInt(animationCurve))
        let options: UIViewAnimationOptions = [.BeginFromCurrentState, curveAnimationOption]

        if let animation = keyboardWillHideAnimation() {
            UIView.animateWithDuration(animationDuration, delay: 0, options: options, animations: animation, completion: nil)
        }
    }

}

I explicitly use where clause in extension KeyboardAnimatable where Self: UIViewController to narrow it down so that I can use self when adding observers. Now I can create a view controller that adopts this protocol and e.g. changes constraint when keyboard shows up:

import UIKit

class ViewController: UIViewController, KeyboardAnimatable {

    @IBOutlet weak var constraintYCenter: NSLayoutConstraint!

    override func viewDidLoad() {
        super.viewDidLoad()
        setupKeyboardNotifcationListeners()
    }

    func keyboardWillShowAnimation() -> (() -> Void)? {
        return {
            self.constraintYCenter.constant = 100
            self.view.layoutIfNeeded()
        }
    }

    func keyboardWillHideAnimation() -> (() -> Void)? {
        return nil
    }

//    func keyboardWillShow(notification: NSNotification) {
//        print("will show")
//    }
//    
//    func keyboardWillHide(notification: NSNotification) {
//        print("will hide")
//    }

    deinit {
        removeKeyboardNotificationListeners()
    }
}

But when I run this code the app crashes with following error:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: '-[KeyboardAnimatableDemo.ViewController keyboardWillShow:]: 
unrecognized selector sent to instance 0x7fb429f192d0'

It looks like my view controller doesn't use keyboardWillShow: method implemented in protocol extension. When I uncomment these two methods implemented directly in ViewController they work (logs are printed) but then whole concept with common keyboard animator is useless.

I thought there might be something weird happening with self in setupKeyboardNotifcationListeners() but even if I change it to e.g. func setupKeyboardNotifcationListeners(vc: UIViewController) and use vc instead of self when registering observer nothing changes.

One possible solution could be also creating UIViewController extension only for a subset of controllers that adopt this protocol e.g. something like this extension UIViewcontroller where Self: KeyboardAnimatable but this one doesn't compile and I haven't found any example of such definition so it may even not be possible in Swift.

Did anyone try to achieve something similar and he succeeded?

Upvotes: 4

Views: 985

Answers (1)

Adam Śliwakowski
Adam Śliwakowski

Reputation: 365

I solved this problem by removing protocol and introducing "empty" implementation for those methods. Now Extension for keyboard extends directly UIViewController so it should be quite easy to use.

Code is on gist: https://gist.github.com/AdamSliwakowski/6fa1e920254ce584d203

Upvotes: 1

Related Questions