Bridging the Gap Between iOS Native Functionality and JavaScript Web Applications
I’ve been recently working on a project which requires some native iOS capabilities that are not accessible within a JavaScript WebView context. There are projects such as Capacitor which help to bridge this gap. However, I already have an existing iOS application that I wished to include this functionality in.
To provide bi-directional communication between the JavaScript application running within the WKWebView
and the native functionality available within the wrapper application, I built a thin bridge placed in the middle.
In this post, I wish to provide an example pattern I have been using which handles the typical asynchronous lifecycle of the given functionality.
Bridging the Gap
To set up the bridge between the native Swift wrapper application and the JavaScript web application, you must implement a class which conforms to the WKScriptMessageHandler
protocol.
With this implementation, you can then register the desired message names (à la function names) to handle within a WKUserContentController
, which is supplied to the WKWebView
instance.
class Example: WKScriptMessageHandler {
weak var webView: WKWebView!
weak var locationManager: CLLocationManager!
// ..
private func initWebView() {
let contentController = WKUserContentController()
contentController.add(self, name: "getCurrentWifiSsid")
let config = WKWebViewConfiguration()
config.userContentController = contentController
self.webView = WKWebView(frame: calcWebviewFrame(), configuration: config)
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
switch message.name {
case "getCurrentWifiSsid":
if let body = message.body as? [String: Any], let requestId = body["requestId"] as? String {
let status = self.locationManager.authorizationStatus
if status != .authorizedWhenInUse && status != .authorizedAlways {
self.evaluateJavascriptWithinWebView("onGetCurrentWifiSsidResult('\(requestId)', null, 'Does not have sufficient WiFi permissions')")
return
}
NEHotspotNetwork.fetchCurrent { network in
if let ssid = network?.ssid {
self.evaluateJavascriptWithinWebView("onGetCurrentWifiSsidResult('\(requestId)', '\(ssid)', null)")
} else {
self.evaluateJavascriptWithinWebView("onGetCurrentWifiSsidResult('\(requestId)', null, null)")
}
}
}
default:
break
}
}
private func evaluateJavascriptWithinWebView(_ js: String) {
DispatchQueue.main.async {
self.webView.evaluateJavaScript(js, completionHandler: nil)
}
}
}
The message handlers are uni-directional and, as such, we need to coordinate the request/response lifecycle ourselves, preferably in an asynchronous manner, so as to not block the main thread.
Thanks to the Promise
construct, we can abstract this away from the JavaScript callee in a very clean manner.
const requests = {};
export const getCurrentWifiSsid = () =>
new Promise((resolve, reject) => {
const requestId = Date.now().toString();
requests[requestId] = { resolve, reject };
window.webkit.messageHandlers.getCurrentWifiSsid.postMessage({
requestId,
});
});
window.onGetCurrentWifiSsidResult = (requestId, ssid, error) => {
if (error) {
requests[requestId].reject(error);
} else {
requests[requestId].resolve(ssid);
}
delete requests[requestId];
};
To achieve this, we create a requestId
within the JavaScript bridge code, storing the Promise’s resolve
and reject
callbacks in a shared space which can be looked up by this requestId
at a later time.
We then send the message (à la function call) on to the native wrapper application, including all desired parameters (including this requestId
).
Upon completion of the native application’s behaviour, a separate JavaScript bridge function is invoked which supplies the requestId
along with any additional parameters.
The JavaScript bridge is then able to look up and invoke the Promise’s resolve
and reject
callbacks accordingly with the desired resulting values.
Aside: we could additionally include a timeout which rejects the Promise after a given time if we do not receive a result back from the native wrapper in a sufficient amount of time.