Dave Feldman
Dave Feldman

Reputation: 194

Modifying keyboard toolbar / accessory view with WKWebView

I'm using a WKWebView with a content-editable div as the core of a rich-text editor, and would like to modify the top toolbar above the keyboard—the one that contains the autocorrect suggestions and formatting buttons. (Not sure if this counts as an input accessory view or not).

I've found a few posts showing how to remove the bar, but none of them seems to work, and ideally I'd like to keep the autocorrect part anyway.

At least one app, Ulysses, does this (though I don't know if it's with a web view):

Ulysses

And indeed, I'm pretty sure I can achieve it by doing surgery on the keyboard view hierarchy...but that seems like a tedious and brittle approach.

Is there a better way?

Thanks!

Upvotes: 10

Views: 7079

Answers (2)

C. Bess
C. Bess

Reputation: 617

Perhaps a more stable (future-proof) way of having the same behavior would be to place the input accessory view in the WKWebView.superview, then use respond to keyboard events, to show and hide it. It's the old-fashioned way of making input accessory views.

Details:

  1. observe keyboard show/hide notifications
  2. place the input accessory view in the WKWebView.superview
  3. on keyboard show: position the input accessory view above the keyboard
  4. on keyboard hide: hide it with keyboard

And perhaps obvious, use delegation to pass action events between the WKWebView and your UIViewController/UIView subclass.

Also, in iOS 13+, you can override the inputAccessoryView. You should be able to replace it to provide your own on iPad. More details here: https://stackoverflow.com/a/58001395/344591

Upvotes: 0

Samantha
Samantha

Reputation: 2289

Maybe this tutorial on UITextInputAssistantItem would be helpful.

That said, after fiddling around with this for a while, using WKWebView I still could only get this to work for the first time the keyboard displayed, and every time after that it would return to its original state. The only thing I found that consistently worked was something like the following:

class ViewController: UIViewController {

    @IBOutlet weak private var webView: WKWebView!
    private var contentView: UIView?

    override func viewDidLoad() {
        super.viewDidLoad()
        webView.loadHTMLString("<html><body><div contenteditable='true'></div></body></html>", baseURL: nil)
        for subview in webView.scrollView.subviews {
            if subview.classForCoder.description() == "WKContentView" {
                contentView = subview
            }
        }
        inputAssistantItem.leadingBarButtonGroups = [UIBarButtonItemGroup(barButtonItems: [UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed(_:)))], representativeItem: nil)]
        inputAssistantItem.trailingBarButtonGroups = [UIBarButtonItemGroup(barButtonItems: [UIBarButtonItem(barButtonSystemItem: .camera, target: self, action: #selector(openCamera(_:))), UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(actionPressed(_:)))], representativeItem: nil)]
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidShow), name: .UIKeyboardDidShow, object: nil)
    }

    @objc private func donePressed(_ sender: UIBarButtonItem) {
        view.endEditing(true)
    }

    @objc private func openCamera(_ sender: UIBarButtonItem) {
        print("camera pressed")
    }

    @objc private func actionPressed(_ sender: UIBarButtonItem) {
        print("action pressed")
    }

    @objc private func keyboardDidShow() {
        contentView?.inputAssistantItem.leadingBarButtonGroups = [UIBarButtonItemGroup(barButtonItems: [UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(donePressed(_:)))], representativeItem: nil)]
        contentView?.inputAssistantItem.trailingBarButtonGroups = [UIBarButtonItemGroup(barButtonItems: [UIBarButtonItem(barButtonSystemItem: .camera, target: self, action: #selector(openCamera(_:))), UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(actionPressed(_:)))], representativeItem: nil)]
    }

}

It will give something like this:

enter image description here

It's a bit frustrating to have to set this on the content view every time the keyboard shows, as well as on the view controller itself, and I hope there's a better way to do this.... But unfortunately I could not find it.

Upvotes: 8

Related Questions