My app has a view similar to the iOS Messages app, with a toolbar-style view containing a text field docked to the bottom of the screen:
When the user taps the text field to enter text, the keyboard slides up and the toolbar slides with it:
I've implemented this by making the toolbar view an inputAccessoryView
on the UITableViewController
and making its main view
the first responder. The main body text "Here is a link…" is implemented as a UIWebView
as it's populated with HTML from a web service and needs to support formatting and links.
The problem I'm having is that when the user taps anywhere on the UIWebView
, it becomes first responder and the inputAccessoryView
hides itself:
Which I don't want to happen. I need the user to be able to tap links in the web view so making it not respond to user interaction isn't an option.
Does the UIWebView
have to become first responder to handle taps? If so, can the webview handle the tap on the link but then pass the touch event through to the main view
and make that first responder again straight away? Any pointers on how to fix this would be gratefully received.
UPDATE:
So it appears that the web view doesn't have to become first responder to handle link taps, as I can hack my way around the issue with:
extension UIView {
public override func becomeFirstResponder() -> Bool {
// Actual view is instance of private class UIWebBrowserView, its parent parent view is UIWebView
if self.superview?.superview is UIWebView {
return false
} else {
return super.becomeFirstResponder()
}
}
}
Can anyone tell me how to achieve the above in a non-hacky fashion?
I can offer two things here. A quick and dirty solution (I hope) and an advice (or you may call it me being surprised).
The dirty solution: Implement something like this
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
self.shouldCloseKeyboard = false
return true
}
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
return self.shouldCloseKeyboard
}
@IBAction func inputDone(_ sender: AnyObject) {
if self.inputField.isFirstResponder {
self.shouldCloseKeyboard = true
self.inputField.resignFirstResponder()
}
}
inputDone:
would be connected to your "Post" button and would obviously also contain whatever else you need to do for posting (or you put that in textFieldShouldEndEditing
if and before you close it, i.e. are done with input). shouldCloseKeyboard
is a Bool
var that's intialized to false
, but then used as you can see here. Obviously the inputField
is your text field (NOT the entire accessory view!) and the entire class is conforming to UITextFieldDelegate
and the text field has its instance set as the delegate
via Interface Builder (or somehow else).
This should behave as you intend. I tried it out in a sample project, though I didn't use a toolbar like you (see below) or put everything into a table. I just did a plain webview with an input below and a button to close stuff. Once the text field was the first responder, i.e. the keyboard was up I could click in the webview, follow links etc. and the keyboard didn't disappear. When clicking the button, it correctly closed (I did not bother what was first responder then).
Now what surprised me: At first I thought it a nice trick to simply use the toolbar as inputAccessoryView
to have it move over the keyboard. However, on further thought I came to the conclusion I would not have done this (and instead manually shift the view up when the keyboard is shown, together with anything else. You probably will have to do that, too, to ensure nothing important is obscured by the keyboard or the accessory view). The reason is that accessory views should not be in the view hierarchy elsewhere already. In fact, when I quickly tried it in my demo project in a straight-forward matter (I embedded the button and text field in a UIView
, defined an outlet for this and then set it as inputAccessoryView
in code, tried at various places) the app crashed. which is reasonable, because the textfield itself is a subview of the container, which would be the accessory, but at the same time a child of the view controller's main view... I am surprised you managed to overcome this all.
In short, that approach seemed like a hassle. The way I understand input accessory views, they're more meant to be used for additional views, not for controls that are already presented on screen (and are thus already part of the view hierarchy). In another project I specifically avoided messing with this by simply faking it: I had a separate view loaded as accessory that was just looking like the control it was "associated" with. Once this fake view was displayed I switched first responder to it (well, to one of its subviews, a text field, so the keyboard stayed up) and my user could edit away. Once a done button was pressed, I copied the input to the "original" text field and all was neato.
So in the long run, I would recommend you (or anyone) to reconsider messing with existing views as accessories and instead opt for "regular" view shifting where needed and stay with accessory views as "additional helpers".
UPDATE:
Okay, what I described above (in the "quick and dirty" solution) was assuming you only want one way out of the edit, i.e. prevent anything else from becoming first responder and only let it lose first responder once "Done" is pressed.
In regards to the webView it accomplishes the same thing. The problem when trying to only prevent the webView from becoming first responder is how to identify what is causing the text field to lose first responder status. I think your "hacky" approach points into the correct direction, but since you should not override methods in an extension (iirc) I'd suggest subclassing the UIWebView in this case. You can then also give it a property turning on and off that behavior (i.e. if the text field becomes first responder/starts editing you switch that on, once it loses it you switch it off). This should be enough for the subclass:
class MyWebView: UIWebView {
public var shouldNotBecomeFirstResponder = false;
override public var canBecomeFirstResponder: Bool {
if self.shouldNotBecomeFirstResponder {
return false
} else {
return super.canBecomeFirstResponder
}
}
}
(I ensured that the original behavior is mimicked if the new Bool
is not set, this should result in the Class behaving exactly like a webView in all other cases. I didn't test that in your setup, though.)
Setting shouldNotBecomeFirstResponder
should obviously done at all appropriate places. I guess you could do that listening for keyboard (dis)appearance notifications, for example.
Generally, there might also be other ways to do that not requiring subclasses. You could perhaps use gesture recognizers in addition to my first suggestion above to figure out whether shouldCloseKeyboard
has to be set or not, but that's probably more work and results in hard to read/maintain code. It might be hard to figure out what gets called first, the logic figuring whether the webView was clicked (and thus shouldCloseKeyboard
has to be set before it actually becomes first responder!) or something else (in which case it has to be unset somehow).
So I#d go with subclassing, even though the subclass adds nearly nothing to the webView. :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With