Did you know that it is possible to call Swift code from the JavaScript code of a web page displayed inside a WKWebView?
Sooner or later every mobile developer in the world had the following specific need: integrate a website page inside an app. Usually the integration to be developed requires a deep integration between web and native: the app must react to some changes in the web page based on the user interactions or some other events (automatic refresh, geolocation etc.). The old way to do this integration was to catch some url change/page load using the classical UIWebView delegate methods. But starting from iOS 8 (this is old but gold ) there’s a better way to do this integration using
WKWebView
s and WKScriptMessageHandler
. In this post I will show you how is it possible to use them to call Swift code from Javascript code inside a webpage.
Suppose for example we have a simple html page that contains a form with 2 input fields and a button. We want to be able to read the form data inserted when the user clicks on the button and do some action on the Swift code side. In this sample case we will show a simple UIAlertController
that contains the form data.
Let’s start by setting up the controller that will display the form, FormViewController
. The first thing to do is to setup the WKWebView
and add it to the main UIView
of our controller. After that we can already setup the code that will load the web page in the function loadPage
(in this case, to keep the example as simple as possible, the webpage is loaded from a local file in the Bundle
).
class FormViewController: UIViewController {
private var wkWebView: WKWebView!
override func viewDidAppear(_ animated: Bool) {
super.viewDidLoad()
self.setupWKWebview()
self.loadPage()
}
private func setupWKWebview() {
self.wkWebView = WKWebView(frame: self.view.bounds, configuration: self.getWKWebViewConfiguration())
self.view.addSubview(self.wkWebView)
}
private func loadPage() {
if let url = Bundle.main.url(forResource: "form", withExtension: "html") {
self.wkWebView.load(URLRequest(url: url))
}
}
...
}
The code is pretty basic here. The important thing that you can see in the piece of code above is the call to a getWKWebViewConfiguration
method. Let’s see the implementation of this method.
class FormViewController: UIViewController, WKScriptMessageHandler {
...
private func getWKWebViewConfiguration() -> WKWebViewConfiguration {
let userController = WKUserContentController()
userController.add(self, name: "observer")
let configuration = WKWebViewConfiguration()
configuration.userContentController = userController
return configuration
}
...
}
This is where the “magic binding” between the Javascript code and the native side happens. In fact, by setting the FormViewController
as WKScriptMessageHandler
using the WKUserContentController
, we will receive each message that is send from the webpage using the message handler window.webkit.messageHandlers.observer
. As you can see, observer
is the name that we specified in the WKUserContentController
configuration during its creation.
After the setup we can implement the WKScriptMessageHandler
protocol method userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
and decide what to do with the form data received. In this case I decided that the message that we will receive will have the following structure:
{
name: '<a name>',
email: '<an email>'
}
This message will be converted to a Swift Dictionary
and will be put in the body
of the WKScriptMessage
received by the WebKit
SDK. So we can proceed with the unwrap of each property of this dictionary and show them in a UIAlertViewController
. Below you can find the final implementation of the FormViewController
.
class FormViewController: UIViewController, WKScriptMessageHandler {
private var wkWebView: WKWebView!
override func viewDidAppear(_ animated: Bool) {
super.viewDidLoad()
self.setupWKWebview()
self.loadPage()
}
private func setupWKWebview() {
self.wkWebView = WKWebView(frame: self.view.bounds, configuration: self.getWKWebViewConfiguration())
self.view.addSubview(self.wkWebView)
}
private func loadPage() {
if let url = Bundle.main.url(forResource: "form", withExtension: "html") {
self.wkWebView.load(URLRequest(url: url))
}
}
private func getWKWebViewConfiguration() -> WKWebViewConfiguration {
let userController = WKUserContentController()
userController.add(self, name: "observer")
let configuration = WKWebViewConfiguration()
configuration.userContentController = userController
return configuration
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let data = message.body as? [String : String], let name = data["name"], let email = data["email"] {
showUser(email: email, name: name)
}
}
private func showUser(email: String, name: String) {
let userDescription = "\(email) \(name)"
let alertController = UIAlertController(title: "User", message: userDescription, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: "OK", style: .default))
present(alertController, animated: true)
}
}
Let’s see now the implementation of the web page. It will contain a standard html form. We attach a call to a submitForm
Javascript function that will get the data from the form (using standard getElementById
calls) and construct the message to be sent. After that, we can send the message with the call window.webkit.messageHandlers.observer.postMessage(message)
. Below you can find the complete implementation of the web page.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>ExploreWKWebViewJavascript</title>
<script type="text/javascript">
function submitForm() {
var message = {
name: document.getElementById("name").value,
email: document.getElementById("email").value
};
window.webkit.messageHandlers.observer.postMessage(message);
}
</script>
</head>
<body>
<div>
<h2>Enter your data:</h2>
<div>
<label for="email">Email:</label>
<input type="email" id="email" placeholder="Enter email" name="email">
</div>
<div>
<label for="name ">Name:</label>
<input type="value" id="name" placeholder="Enter name" name="name">
</div>
<button onclick="submitForm()">Click me</button>
</div>
</body>
</html>
One important thing to note: window.webkit.messageHandlers
is expose on the window
as a global object only when your page is displayed on a iOS device inside a WKWebView
. This means that if you’re planning to show your webpage with this kind of binding also on other platform, you will need to implement a guard on the webkit.messageHandlers
object.
You can find the complete example in this github repository.
The WKWebView
and the WKScriptMessageHandler
are really powerful. They let you implement a deeper web to native integration that could significantly improve the general user experiences. WKScriptMessageHandler
, another useful tool in the toolbox of every iOS Developer .
During the last months I worked a lot with Spring Boot backend applications. In this post I explain how you can consume a REST api from a Spring Boot application using RestTemplate and (the new) WebClient.
Read MoreRecently I upgraded my ID3TagEditor swift package to the latest Swift tools version (5.3). During the upgraded I discovered that now you can bundle reources with your Swift package. In this post I will show you how you can do this, and also a interesting trick in order to be able to build a project as a Swift Package and as a standard project from Xcode.
Read MoreRecently I migrated my website to Webpack and TypeScript. I decided also to give a try to Workbox, a set of Google libraries to improve the creation of a Progressive Web App. Let’s see how easy it is to create a PWA with this tools.
Read More