WKWebView. Return a value from native code to JavaScript

WKWebView exposes a comfortable way to call native code from JavaScript. So called Message handlers are defined in native code and can be later used in JavaScript like this:

webkit.messageHandlers.<handler>.pushMessage(message)

But what about return values? For as you know, WKWebView runs in its own process and that’s why pushMessage() function cannot return any value. It is not possible to get the return value from native function synchronously. It is also not possible to give a JS callback function to native function. So what to do, if we need a return value from native function?

There are several approaches how to realize and organize asynchronous communication of native code <-> JavaScript with WKWebView. The first thought is to implement another JS function that would be called from native code after operation on native side is finished, i.e.

JavaScript code:



function readStringFromFile(relativeFilePath) {
   webkit.messageHandlers.readFileHandlder.pushMessage({filePath: relativeFilePath});
}

// this function will be later called by native code
function readStringFromFileReturn(returnedValue) {
   // do you stuff with returned value here
}

And the corresponding handler implementation in Objective-C would look like this:

- (void)userContentController:(WKUserContentController*)userContentController
      didReceiveScriptMessage:(WKScriptMessage*)message {
    
    if([message.name isEqualToString:@"readFileHandler"]) {
        NSDictionary *paramDict = message.body;        
        NSString *filePath = [paramDict objectForKey:@"filePath"];
        NSString *resultString = [self readStringFromFileSynchronously: filePath];
        NSString *javaScript = [NSString stringWithFormat:@"readStringFromFileReturn('%@');", resultString];
        [self.webView evaluateJavaScript:javaScript completionHandler:nil];     
    }     
}

The described approach is not very elegant. If you have several functions in your interface between JavaScript and native code, the chained calls get very quickly confusing.

Much better approach is utilizing relative new concept of promises in JavaScript. I think, it’s also the way, Apple bore in mind when WKWebView has been introduced. More about promises can be read here or here. First, we need to extend our JavaScript:


// object for storing references to our promise-objects
var promises = {}

// generates a unique id, not obligator a UUID
function generateUUID() {
    var d = new Date().getTime();
    var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                                                              var r = (d + Math.random()*16)%16 | 0;
                                                              d = Math.floor(d/16);
                                                              return (c=='x' ? r : (r&0x3|0x8)).toString(16);
                                                              });
    return uuid;
};

// this funciton is called by native methods
// @param promiseId - id of the promise stored in global variable promises
function resolvePromise(promiseId, data, error) {
    if (error){
         promises[promiseId].reject(data);
        
    } else{
       promises[promiseId].resolve(data);
    }
    // remove referenfe to stored promise
    delete promises[promiseId];
}


Our readStringFromFile function looks now like this:


function readStringFromFile(relativeFilePath) {
   var promise = new Promise(function(resolve, reject) {                                  
                                  // we generate a unique id to reference the promise later
                                  // from native function
                                  var promiseId = generateUUID();
                                  // save reference to promise in the global variable
                                  this.promises[promiseId] = { resolve, reject};
                                  
                                  try {
                                    // call native function
                                    window.webkit.messageHandlers.readFileHandler.postMessage({promiseId: promiseId, fileName: fileName}); 
                                  }
                                  catch(exception) {
                                    alert(exception);
                                  }
                                  
                                  });
        
        return promise;

}


The JavaScript code is self explaining. One important thing to mention is that the reference to the created promise-object is stored globally.

And now the Objective-C code:

- (void)userContentController:(WKUserContentController*)userContentController
      didReceiveScriptMessage:(WKScriptMessage*)message {
    
    if([message.name isEqualToString:@"readFileHandler"]) {
        NSDictionary *paramDict = message.body;
        NSString *promiseId = [paramDict objectForKey:@"promiseId"];
        NSString *filePath = [paramDict objectForKey:@"filePath"];
        NSString * resultString = [self readStringFromFileSynchronously: filePath];
        NSString *javaScript = [NSString stringWithFormat:@"resolvePromise('%@',%@, null);",promiseId, resultString];        
        [self.webView evaluateJavaScript:javaScript completionHandler:nil];     
    }     
}

As you can see the native function calls resolvePromise with return value. The code at JavaScript continues running, after the promise is resolved. What we are still missing, is the usage example for function readStringFromFile, which returns a promise object:


readStringFromFile("someRelativePath").then(function(returnedString) {
    var returnValue = returnedString;
    logToConsole(returnValue);
    // do your stuff here

  }, function(returnedString) {
      logToConsole('error');
  });

Utilization of promises as described above makes code more readable.It allows a clean definition of the interface between JavaScript and native code.

There are also other projects, aiming to enable easy communication between JavaScript in WKWebView and native code, e.g. XWebView. However, when integrating such components in your project be aware of:

  • you become dependent on third party development. Each newer version of SWIFT would make you to wait for compatible updates
  • they use the same messaging mechanism under the hood