5

I have a Cocoa Touch Framework named FooFramework.

Within it, I want to manage the move up on the Y axis for selected views when the keyboard shows. I created a KeyboardManager class. Here's how it looks:

import UIKit

public class KeyboardManager {
    var notifyFromObject: Any?
    var observer: Any
    public var viewsToPushUp: [UIView] = []

    public init(observer: Any, viewsToPushUp: [UIView], notifyFromObject: Any? = nil) {
        self.observer = observer
        self.notifyFromObject = notifyFromObject
        self.viewsToPushUp = viewsToPushUp
    }

    public func pushViewsUpWhenKeyboardWillShow(){
        let notificationCenter = NotificationCenter.default
        print(self)
        notificationCenter.addObserver(self.observer, selector: #selector(FooFramework.KeyboardManager.pushViewsUp), name: NSNotification.Name.UIKeyboardWillShow, object: notifyFromObject)
    }

    @objc public func pushViewsUp(notification: NSNotification) {
        if let keyboardRectValue = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
            let keyboardHeight = keyboardRectValue.height

            for view in viewsToPushUp {
                view.frame.origin.y -= keyboardHeight
            }
        }
    }
}

Then, I import this FooFramework in an iOS app named Bar. To test the FooFramework, I want to push up a UITextField. Here's the code:

import UIKit
import FooFramework

class ViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        let kb = KeyboardManager(observer: self, viewsToPushUp: [textField], notifyFromObject: nil)
        kb.pushViewsUpWhenKeyboardWillShow()
    }

    func pushViewsUp(notification: NSNotification) {
        print("This should not be printed")
    }
}

My problem is that This should not be printed appears in the console and the pushViewsUp method from the KeyboardManager never gets called. Even though I used a fully qualified name for the selector, it insists on using the pushViewsUp from the ViewController. This is driving me nuts.

If I remove pushViewsUp from the ViewController, I get the following error:

Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Bar.ViewController pushViewsUpWithNotification:]: unrecognized selector sent to instance 0x7fc540702d80'

What do I need to do so the selector properly points to FooFramework.KeyboardManager.pushViewsUp?

2 Answers 2

4

I believe you need to use self instead of self.observer for the observer in the addObserver function.

Also you need to declare the kb variable outside the scope of the function in order for the manager to detect the notification.

Example:

import UIKit
import FooFramework

class ViewController: UIViewController {

    @IBOutlet weak var textField: UITextField!

    var kb: KeyboardManager?

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        kb = KeyboardManager(observer: self, viewsToPushUp: [textField], notifyFromObject: nil)
        kb?.pushViewsUpWhenKeyboardWillShow()
    }
}

KeyboardManager changes:

public func pushViewsUpWhenKeyboardWillShow() {
    let notificationCenter = NotificationCenter.default
    print(self)
    notificationCenter.addObserver(self, 
                                   selector: #selector(FooFramework.KeyboardManager.pushViewsUp), 
                                   name: NSNotification.Name.UIKeyboardWillShow, 
                                   object: notifyFromObject)
}
Sign up to request clarification or add additional context in comments.

1 Comment

Thank you. I completely overlooked that it's actually the class itself registering the event.
2

Other than what Justin has suggested which is all correct, there are a few more things to consider before you fully solve the problem.

  1. KeyBoardManager class instance itself is going to observe the keyboardWillMoveUp notification so your

var observer: Any

within it is unnecessary. You should remove that.

  1. I would also put the addObserver part right in the init of KeyBoardManager class itself so that this extra call pushViewsUpWhenKeyboardWillShow() can be avoided which seems to be doing nothing but that. Since the KeyBoardManager class is supposed to be doing only this, I don't see why adding observer should be another function call.

So this is how your KeyboardManager class should look:

import UIKit

public class KeyboardManager {

    var notifyFromObject: Any?
    public var viewsToPushUp: [UIView] = []

    public init(viewsToPushUp: [UIView], notifyFromObject: Any? = nil){
        self.notifyFromObject = notifyFromObject
        self.viewsToPushUp = viewsToPushUp

//remember KeyboardManager itself is observing the notifications and moving the views it received from the ViewController. Hence we use self.
        NotificationCenter.default.addObserver(self, selector: #selector(FooFramework.KeyboardManager.pushViewsUp), name: NSNotification.Name.UIKeyboardWillShow, object: notifyFromObject)
    }

    @objc public func pushViewsUp(notification: NSNotification) {
        if let keyboardRectValue = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
            let keyboardHeight = keyboardRectValue.height

            for view in viewsToPushUp {
                view.frame.origin.y -= keyboardHeight
            }
        }
    }
}
  1. You will also need to work with the frames properly before you get the right behavior out of this.

  2. You should extend the lifespan of your KeyboardManagerInstance to live as long as the ViewController which has the textField is alive. You do it by declaring it as an instance variable inside the ViewController as Justin has suggested. The way you were doing it, your KeyboardManager instance is a local variable which is created and immediately released as soon as the function goes out of scope. To verify this, you can add this to your KeyboardManager class and check:

      deinit {
        print("KeyboardManager is perhaps dying an untimely death.")
      }
    

Finally your ViewController class should do just this

  import UIKit
  import FooFramework

  class ViewController: UIViewController {

@IBOutlet weak var textField: UITextField!

var kb: KeyboardManager?

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    kb = KeyboardManager(viewsToPushUp: [textField], notifyFromObject: nil)
   //observe that there is no observer param in the initializer and also no "pushViewsUpWhenKeyboardWillShow" call as that behavior has already been moved to init itself.
}

}

2 Comments

Thanks but I don't want to have the addObserver in the init. I plan on adding several other keyboard management methods and want to let the user choose which ones he wants to use.
In that case, what you were doing seems to be the right approach.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.