Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can a UIWebView handle user interaction without becoming first responder?

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?

like image 475
fractious Avatar asked Oct 03 '16 10:10

fractious


1 Answers

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. :)

like image 56
Gero Avatar answered Nov 15 '22 11:11

Gero