0

I have a need for an external Keyboard Manager class since I have multitude of Viewcontrollers that require that service. The code I have written works perfectly when it sits physically inside the Viewcontroller, but once I call it through Keyboard Manager class i get "libc++abi.dylib: terminating with uncaught exception of type NSException".

I know this error normally means that there is some connection broken with the IBOutlet, but in this case I am not sure how this can relate.

The class basically tells the scrollview in viewcontroller that calls it to scroll up and make a textfield visible, restore the state when editing is finished and hide the keyboard when scrollview is tapped. I also tried same approach with KeyboardManagerDelegate, with identical results. I've also tried changing the activeField, vc and scrollView variables to weak in any possible combination but it did not change anything.

Here's the code for KeyboardManager:

import UIKit

class KeyboardManager {
    var activeField: UITextField?
    var vc: UIViewController!
    var scrollView: UIScrollView!

    init(_ vc: UIViewController, _ scrollView: UIScrollView, _ activeField: UITextField?) {
        self.vc = vc
        self.scrollView = scrollView
        self.activeField = activeField
        NotificationCenter.default.addObserver(vc, selector: #selector(keyboardDidShow), name: .UIKeyboardDidShow, object: nil)
        NotificationCenter.default.addObserver(vc, selector: #selector(keyboardWillHide), name: .UIKeyboardWillHide, object: nil)

        let tap = UITapGestureRecognizer(target: vc, action: #selector(hideKeyboard))
        tap.numberOfTouchesRequired = 1
        self.scrollView.addGestureRecognizer(tap)
    }

    @objc func keyboardDidShow(notification: NSNotification) {
        if let activeField = activeField, let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
            print("Keyboard height is \(keyboardSize.height)")
            let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: keyboardSize.height, right: 0.0)
            scrollView.contentInset = contentInsets
            scrollView.scrollIndicatorInsets = contentInsets
            scrollView.scrollRectToVisible(activeField.frame, animated: true)
        }
    }

    @objc func keyboardWillHide(notification: NSNotification) {
        let contentInsets = UIEdgeInsets.zero
        scrollView.contentInset = contentInsets
        scrollView.scrollIndicatorInsets = contentInsets
    }

    @objc func hideKeyboard() {
        activeField?.resignFirstResponder()
    }

}

The relavant code in Viewcontroller is here:

class ViewController: UIViewController, UITextFieldDelegate {

    weak var activeField: UITextField?
    var keyboardManager: KeyboardManager!
    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var textField: UITextField!


override func viewDidLoad() {
        super.viewDidLoad()
        keyboardManager = KeyboardManager(self, scrollView, activeField)
}

    func textFieldDidBeginEditing(_ textField: UITextField) {
        self.activeField = textField
    }

    func textFieldDidEndEditing(_ textField: UITextField) {
        self.activeField = nil
    }
}

Thanks in advance for any insights on how to get this working as my project is now on hold until I can figure this out, it would be just too much repetition if I would to copy & paste this code into every single Viewcontroller I have.

UPDATE: I just found something interesting that I did not see before in my own project: KeyboardManagerDrama[1731:3465043] Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[KeyboardManagerDrama.ViewController keyboardDidShowWithNotification:]: unrecognized selector sent to instance 0x15fe18790' So Im thinking perhaps it is because im setting the observer to vc but the viewcontroller does not have the selector functions. If this is the case, how can I set the observers to viewcontroller while keeping selector functions in the KeyboardManager?

rp777
  • 21
  • 4
  • Any way, `vc` `activeField ` `scrollView` in `KeyboardManager` must be weak. Do you have more error log? – trungduc Oct 31 '17 at 03:05
  • That's the only error I see in the console and it happens immediately when I select the textfield in viewcontroller. I tried making all the variables weak, made no difference. – rp777 Oct 31 '17 at 04:50
  • Let add an exception break point and figure out what line make this crash – trungduc Oct 31 '17 at 04:54
  • Where would you add it? I have the Obj-C exception breakpoints switched on, and this one is triggered in the line **"class AppDelegate: UIResponder, UIApplicationDelegate {"**, when I step in it results in "Thread 1: signal SIGABRT" as well as "libc++abi.dylib: terminating with uncaught exception of type NSException" – rp777 Oct 31 '17 at 06:19
  • I mean this https://stackoverflow.com/questions/17802662/exception-breakpoint-in-xcode – trungduc Oct 31 '17 at 06:29
  • Yes, I have **All Objective-C Exceptions activated**, I get the Exception described above and stepping into it is taking me directly to assembly mode not showing any Swift code. – rp777 Oct 31 '17 at 06:51
  • It's hard to debug. Can you create a small project with this bug and push it to github? I want to debug it myself. – trungduc Oct 31 '17 at 07:02
  • Sure, I can :) Give me 10 mins – rp777 Oct 31 '17 at 07:42
  • Here you go. Hope for my and everyone elses sake we get to the bottom of this: https://github.com/raul7angels/KeyboardManagerDrama – rp777 Oct 31 '17 at 08:29
  • I just found something interesting that I did not see before in my own project: KeyboardManagerDrama[1731:3465043] ** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[KeyboardManagerDrama.ViewController keyboardDidShowWithNotification:]: unrecognized selector sent to instance 0x15fe18790' ** So Im thinking perhaps it is because im setting the observer to vc but the viewcontroller does not have the selector functions. If this is the case, how can I set the observers to viewcontroller while keeping selector functions in the KeyboardManager? – rp777 Oct 31 '17 at 08:35
  • Right, but you don't need add observer to `viewcontroller`. I have added an answer. Please take a look. – trungduc Oct 31 '17 at 08:39
  • I don't know why you need add observer to `ViewController`, because as i see, you don't have any spacial method to handle notification in your `ViewController` – trungduc Oct 31 '17 at 08:41
  • Because the keyboard appears in the Viewcontroller :) I tried already as suggested from my own thinking, it does not crash but also it does not work... (same like I had no KeyboardManager) – rp777 Oct 31 '17 at 08:46

2 Answers2

0

The reason for crash is you added wrong observer for notification. Try this code below.

NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), name: .UIKeyboardDidShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: .UIKeyboardWillHide, object: nil)

let tap = UITapGestureRecognizer(target: self, action: #selector(hideKeyboard))

Replace vc with self. In your ViewController

You don't have methods keyboardDidShow, keyboardWillHide, hideKeyboard. That's why you got crash

One more, you should replace activeField with textField when creating KeyboardManager in ViewController like this:

keyboardManager = KeyboardManager(self, scrollView, textField)

trungduc
  • 11,926
  • 4
  • 31
  • 55
  • Tried this already, but then the observer doesn't work on the Viewcontroller. We need somehow set observer to vc while keeping selectors in the local class.. How to do that? – rp777 Oct 31 '17 at 08:44
  • If you want this, don't replace `vc` with `self`. Add these 3 methods `keyboardDidShow`, `keyboardWillHide`, `hideKeyboard` to your viewcontroller class and do want you want in each method. – trungduc Oct 31 '17 at 08:49
  • But this beats the purpose of having KeyboardManager class if 60% of the code needs to sit in every Viewcontroller. I want a solution that makes my viewcontrollers lean. Isn't there a way to tell selector where the functions are? I mean this has to be something other people have solved in the past... – rp777 Oct 31 '17 at 08:51
  • So when keyboard show, what do you want in every `viewcontroller` class? – trungduc Oct 31 '17 at 08:52
  • I want the textfield to scroll up so user can see what they are writing :) And when they finish I want the scrollview to restore.. Also I want tapping on the screen to hide the keyboard. There is a longer description of that in the original question too :) – rp777 Oct 31 '17 at 08:53
  • But as i know, not of all `viewcontroller` has `textField`. Each `viewcontroller` will do a different work when key board show and hide. Right? – trungduc Oct 31 '17 at 08:55
  • Not in my case, I have 10+ viewcontrollers that need to do just that. That's the only place you are using keyboard anyway - the textfields, isn't it.... If viewcontroller doesn't have textfields I do not use the KeyboardManager class :) Yet this is outside the scope of this question... PS. I am really thankful for your help and dedication, but I can not accept the answer as it stands because I still have the same problem, even tho we got 1 step closer... – rp777 Oct 31 '17 at 08:57
  • So now with each `viewcontroller` you only have a textField. And if textField appears when keyboard show, it will be moved up? – trungduc Oct 31 '17 at 08:59
  • You have the example project that has all the exact constraints. If you can get it working there then we are good :) Problem is clear now and it does not relate to text field, but to the fact how the observers work. They look for a selector function from the place where you are setting observers to. But what we need to figure out is how to set observer to B while the selectors are called in A. That's very clear now... I tried to google and look StackOverflow, but found nothing... – rp777 Oct 31 '17 at 09:03
  • You can `set observer to B while the selectors are called in A`. But you still need some methods in A and use these methods with `delegate` of B. – trungduc Oct 31 '17 at 09:11
  • Thanks, let me know if you get the demoproject to do that :) I am trying something as well.. Lets see how we go – rp777 Oct 31 '17 at 09:15
  • Thanks. Did you test it yourself? Im running it and It does not do anything :) And I know why. Because it has nothing to do with viewcontroller other than calling these empty delegatefunctions :) – rp777 Oct 31 '17 at 10:41
  • Yes, it’s demo for observer to B while the selectors are called in A – trungduc Oct 31 '17 at 10:42
  • You are not setting observer on B, you are setting it on A (self). – rp777 Oct 31 '17 at 10:51
  • in this case B is self and A is controller – trungduc Oct 31 '17 at 10:54
  • No, sorry, I found out a real problem. It was related to activeField property. So I dont know now, half of the problem you solved and half I solved. How shall we finalise this. Maybe you can just add this to the viewcontroller and I will give you the points: weak var activeField: UITextField? { didSet { keyboardManager.activeField = activeField } } – rp777 Oct 31 '17 at 11:17
  • Yes, other than using self instead of vc. – rp777 Oct 31 '17 at 11:34
  • Congrature @rp777. At least, you fixed it ;) – trungduc Oct 31 '17 at 11:41
0

The code had 2 issues: First the observer was set on a viewcontroller when it was supposed to be set on self like @trungduc said:

NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), name: .UIKeyboardDidShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: .UIKeyboardWillHide, object: nil)

let tap = UITapGestureRecognizer(target: self, action: #selector(hideKeyboard))

Secondly, the activeField property was not updated in KeyboardManager instance so adding this to Viewcontroller solved the problem 100%:

weak var activeField: UITextField? { 
    didSet { 
          keyboardManager.activeField = activeField 
    } 
}
rp777
  • 21
  • 4
  • Actually i dont know why we need update activeField because it’s added in init method – trungduc Oct 31 '17 at 11:58
  • I didnt know either, but you can try it yourself with a breakpoint, activeField never gets a value... – rp777 Oct 31 '17 at 12:00
  • So activeField and textField are different? In itit method you pass activeField, of course it’s nil. But if you pass textField, it works fine. If textField and activeField is a object, problem is you passed wrong object in init method of KeyboardManager. It’s not about activeField. I only see one textField in storyBoard file – trungduc Oct 31 '17 at 12:05
  • Try it :) You will know – rp777 Oct 31 '17 at 13:39
  • I tried it, it works fine with `keyboardManager = KeyboardManager(vc: self, scrollView: scrollView, activeField: textField)`. I think i should edit my answer to add this code. – trungduc Oct 31 '17 at 13:42
  • I tried your code and once i clicked the textfield the keyboard just covered it :) It worked for you? – rp777 Oct 31 '17 at 14:00
  • I can see in your code, you set `activeField = textField`. You use 2 property for 1 textField. There is only a different between `activeField` and `textField` is that `textField` always has value and `activeField` has value after `textFieldDidBeginEditing` – trungduc Oct 31 '17 at 14:04
  • So use `textField` in instance method is right way but not set `activeField` = `textField` in `textFieldDidBeginEditing`. – trungduc Oct 31 '17 at 14:05