How do I get the selected text from a WKWebView from objective-C
Here is how I managed to do that. Not a perfect solution, but good enough.
General explanation
It seems that anything that happens inside the WKWebView must be managed in Javascript. And Apple provides a framework for exchanging information between the Javascript world and the Objective-C (or Swift) world. This framework is based on some messages being sent from the Javascript world and caught in the Objective-C (or Swift) world via a message handler that can be installed in the WKWebView.
First step - Install the message handler
In the objective-C (or Swift) world define an object that will be responsible for receiving the messages from the Javascript world. I used my view controller for that. The code below installs the view controller as a "user content controller" that will receive events named "newSelectionDetected" that can be sent from Javascipts
- (void)viewDidLoad
{
[super viewDidLoad];
// Add self as scriptMessageHandler of the webView
WKUserContentController *controller = self.webView.configuration.userContentController ;
[controller addScriptMessageHandler:self
name:@"newSelectionDetected"] ;
... the rest will come further down...
Second step - Install a Javascript in the view
This Javascript will detect selection change, and send the new selection through a message named "newSelectionDetected"
- (void) viewDidLoad
{
...See first part up there...
NSURL *scriptURL = .. URL to file DetectSelection.js...
NSString *scriptString = [NSString stringWithContentsOfURL:scriptURL
encoding:NSUTF8StringEncoding
error:NULL] ;
WKUserScript *script = [[WKUserScript alloc] initWithSource:scriptString
injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
forMainFrameOnly:YES] ;
[controller addUserScript:script] ;
}
and the Javascript :
function getSelectionAndSendMessage()
{
var txt = document.getSelection().toString() ;
window.webkit.messageHandlers.newSelectionDetected.postMessage(txt) ;
}
document.onmouseup = getSelectionAndSendMessage ;
document.onkeyup = getSelectionAndSendMessage ;
document.oncontextmenu = getSelectionAndSendMessage ;
Third step - receive and treat the event
Now, every time we have a mouse up or a key up in the WKWebView, the selection (possibly empty) will be caught and send to the Objective-C world through the message.
We just need a handler in the view controller to handle that message
- (void) userContentController:(WKUserContentController*)userContentController
didReceiveScriptMessage:(WKScriptMessage*)message
{
// A new selected text has been received
if ([message.body isKindOfClass:[NSString class]])
{
...Do whatever you want with message.body which is an NSString...
}
}
I made a class which inherits from WKWebView, and has a NSString property 'selectedText'. So what I do in this handler, is to store the received NSString in this property.
Fourth step - update the contextual menu
In my daughter class of WKWebView, I just override the willOpenMenu:WithEvent: method to add a menu item if selectedText is not empty.
- (void) willOpenMenu:(NSMenu*)menu withEvent:(NSEvent*)event
{
if ([self.selectedText length]>0)
{
NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:@"That works !!!"
action:@selector(myAction:)
keyEquivalent:@""] ;
item.target = self ;
[menu addItem:item] ;
}
}
- (IBAction) myAction:(id)sender
{
NSLog(@"tadaaaa !!!") ;
}
Now why isn't that ideal ? Well if your web page already sets onmouseup or onkeyup, I override that.
But as I said, good enough for me.
Edit : I added the document.oncontextmenu line in the javascript, that solved the strange selection behavior I sometimes had.
Swift 5 translation
webView.configuration.userContentController.add(self, name: "newSelectionDetected")
let scriptString = """
function getSelectionAndSendMessage()
{
var txt = document.getSelection().toString() ;
window.webkit.messageHandlers.newSelectionDetected.postMessage(txt);
}
document.onmouseup = getSelectionAndSendMessage;
document.onkeyup = getSelectionAndSendMessage;
document.oncontextmenu = getSelectionAndSendMessage;
"""
let script = WKUserScript(source: scriptString, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
webView.configuration.userContentController.addUserScript(script)
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// Use message.body here
}