Rich
Rich

Reputation: 83

iOS Keyboard: textDocumentProxy.documentContextBeforeInput.isEmpty Unexpectedly Returning Nil (when it shouldn't)

I'm working on a custom iOS keyboard – much of it is in place, but I'm having trouble with the delete key and more specifically with deleting whole words.

The issue is that self.textDocumentProxy.documentContextBeforeInput?.isEmpty returns Nil even if there are characters remaining in the text field.

Here's the background: In case you're not familiar, the way it works on the stock iOS keyboard is that while holding the backspace key, the system deletes a character at a time (for the first ~10 characters). After 10 characters, it starts deleting whole words.

In the case of my code, I'm deleting 10 single characters and then I successfully delete a couple of whole words, then suddenly self.textDocumentProxy.documentContextBeforeInput?.isEmpty returns Nil, even if there are characters remaining in the text field.

I've looked all over documentation and the web and I don't see anyone else with the same issue, so I'm sure I'm missing something obvious, but I'm baffled.

Here are the relevant parts of my class definition:

class KeyboardViewController: UIInputViewController { 

//I've removed a bunch of variables that aren't relevant to this question.

    var myInputView : UIInputView {
    return inputView!
}
private var proxy: UITextDocumentProxy {
    return textDocumentProxy
}
override func canBecomeFirstResponder() -> Bool {
    return true
}

override func viewDidLoad() {
    super.viewDidLoad()
    //proxy let proxy = self.textDocumentProxy


    NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillAppear"), name: UIKeyboardWillShowNotification, object: nil)
    NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide"), name: UIKeyboardWillHideNotification, object: nil)


    self.myInputView.translatesAutoresizingMaskIntoConstraints = true

    // Perform custom UI setup here
    view.backgroundColor = UIColor(red: 209 / 255, green: 213 / 255, blue: 219 / 255, alpha: 1)

    NSNotificationCenter.defaultCenter().addObserver(self, selector: "touchUpInsideLetter:", name: "KeyboardKeyPressedNotification", object: nil)

    showQWERTYKeyboard()
}

I setup the listeners and actions for the backspace button as I'm also configuring a bunch of other attributes on the button. I do that in a Switch – the relevant part is here:

case "<<":
            isUIButton = true
            normalButton.setTitle(buttonString, forState: UIControlState.Normal)
            normalButton.addTarget(self, action: "turnBackspaceOff", forControlEvents: UIControlEvents.TouchUpInside)
            normalButton.addTarget(self, action: "turnBackspaceOff", forControlEvents: UIControlEvents.TouchUpOutside)
            normalButton.addTarget(self, action: "touchDownBackspace", forControlEvents: UIControlEvents.TouchDown)
            let handleBackspaceRecognizer : UILongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: "handleBackspaceLongPress:")
            normalButton.addGestureRecognizer(handleBackspaceRecognizer)

            buttonWidth = standardButtonWidth * 1.33
            nextX = nextX + 7

Thanks for any thought you can offer.

****Modifying the original post to help shed more light on the issue**** Here are the 4 functions that are designed to create the backspace behavior. They appear to work properly for at least the first two whole words, but then the optional checking that was correctly suggested starts evaluating to nil and it stops deleting.

    //Gets called on "Delete Button TouchUpInside" and "Delete Button TouchUpOutside"
func turnBackspaceOff() {

    self.backspaceIsPressed = false
    keyRepeatTimer.invalidate()

}

//Handles a single tap backspace
func touchDownBackspace() {

    (textDocumentProxy as UIKeyInput).deleteBackward()

}

//Handles a long press backspace
func handleBackspaceLongPress(selector : UILongPressGestureRecognizer) {

    if selector.state == UIGestureRecognizerState.Began {
        self.backspaceIsPressed = true
        self.keyRepeatTimer = NSTimer.scheduledTimerWithTimeInterval(0.1, target: self, selector: "backspaceRepeatHandlerFinal", userInfo: nil, repeats: true)
        print("handleBackspaceLongPress.Began")
    }
    else if selector.state == UIGestureRecognizerState.Ended {
        self.backspaceIsPressed = false
        keyRepeatTimer.invalidate()
        numberOfKeyPresses = 0
        print("handleBackspaceLongPress.Ended")
    }
    else {
        self.backspaceIsPressed = false
        keyRepeatTimer.invalidate()
        numberOfKeyPresses = 0
        print("handleBackspaceLongPress. Else")
    }

}

func backspaceRepeatHandlerFinal() {

        if let documentContext = proxy.documentContextBeforeInput as String? {
            print(documentContext)
        }

        print("backspaceRepeatHandlerFinal is called")
        if self.backspaceIsPressed {
            print("backspace is pressed")
            self.numberOfKeyPresses = self.numberOfKeyPresses + 1

            if self.numberOfKeyPresses < 10 {

                    proxy.deleteBackward()

            }
            else {

                if let documentContext = proxy.documentContextBeforeInput as NSString? {
                        let tokens : [String] = documentContext.componentsSeparatedByString(" ")
                        var i : Int = Int()
                    for i = 0; i < String(tokens.last!).characters.count + 1; i++ {
                            (self.textDocumentProxy as UIKeyInput).deleteBackward()
                        }

                }
                else {
                    print("proxy.documentContextBeforeInput was nil")
                    self.keyRepeatTimer.invalidate()
                    self.numberOfKeyPresses = 0
                }
            }
        }
        else {
            print("In the outer else")
            self.keyRepeatTimer.invalidate()
            self.numberOfKeyPresses = 0
        }
}

Finally, I don't fully understand why, but XCode automatically inserted these two functions below when I created the keyboard extension. I've modified them slightly in an effort to try to get this to work.

    override func textWillChange(textInput: UITextInput?) {
    // The app is about to change the document's contents. Perform any preparation here.
    super.textWillChange(textInput)
}

override func textDidChange(textInput: UITextInput?) {
    // The app has just changed the document's contents, the document context has been updated.

    var textColor: UIColor
    //let proxy = self.textDocumentProxy
    if proxy.keyboardAppearance == UIKeyboardAppearance.Dark {
        textColor = UIColor.whiteColor()
    } else {
        textColor = UIColor.blackColor()
    }
    super.textDidChange(textInput)
}

Upvotes: 1

Views: 1942

Answers (1)

Alexander Perechnev
Alexander Perechnev

Reputation: 2837

Let's describe what the line below is doing:

if !((self.textDocumentProxy.documentContextBeforeInput?.isEmpty) == nil) {

First, it takes an object marked as optional (by the ? letter):

let documentContext = self.textDocumentProxy.documentContextBeforeInput

Then, it tries to read it's property called isEmpty:

let isEmpty = documentContext?.isEmpty

and then it evaluates the contition:

if !(isEmpty == nil) {

There are two mistakes. The first one is that you are comparing Bool value with nil. Another one is that you aren't sure that documentContext is not a nil.

So, let's write your code in a more appropriate way:

if let documentContext = self.textDocumentProxy.documentContextBeforeInput { // Make sure that it isn't nil
    if documentContext.isEmpty == false { // I guess you need false?
        // Do what you want with non-empty document context
    }
}

Upvotes: 2

Related Questions