I have a text entry form implemented as a UITableViewController where each field in the form is a subview of a cell in the tableview. On my keyboard I have "next" and "prev" buttons that do the expected and call becomeFirstResponder() on the text field one cell before or one cell after the current first responder.
This call always succeeds when "next" is pressed: the call to the next field's becomeFirstResponder() returns true, even if that cell is currently below the bottom of the screen. However, when "prev" is pressed, the call to the previous field's becomeFirstResponder() returns true if that field's parent view is currently visible on the screen but returns false when it is above the top of the screen.
Some things I've considered and eliminated as the cause:
1) The table view uses static cells and therefore the parent cell of the targeted field is not recycled for reuse. It is part of the view hierarchy at all times and the controller keeps a reference to it.
2) The canBecomeFirstResponder property of the field I'm asking to become first responder returns true, even when not visible.
3) The current first responder is not blocking it by refusing to give up first responder status. first responder == nil at the time becomeFirstResponder() is called on the relevant field.
4) The delegate of the field I'm asking to become first responder returns "true" when its shouldBeginEditing(_) field is invoked for the relevant text field.
My question is, given (1) - (4) above, what could be causing becomeFirstResponder() to return 'false'? I've tried but have been unsuccessful in finding more detail about UIResponder's implementation of that method.
Here is the handler for the prev press and what the state looks like at that time. (The while loop is just for debugging of course.)
func keyboardControls(_ keyboardControls: BSKeyboardControls, selectedField field: UIView, inDirection direction: BSKeyboardControlsDirection) {
var loopCount = 1
let mainWindow = AppDelegate.appDelegate().window!
while field.isFirstResponder == false {
print(loopCount) // will print from 1 to overflow
print(field.canBecomeFirstResponder) // prints true
print(field.window == mainWindow) // prints true
print(tableView.subviews.contains(field.superview!.superview!)) // prints true
print(mainWindow.currentFirstResponder()) // prints currently active field on first pass, nil thereafter
field.becomeFirstResponder() // returns false
loopCount += 1
}
}