How to Establish a Two-Way Communication Bridge Between a Native iOS Swift App and JavaScript Using WKWebView
Learn how to establish a seamless two-way communication bridge between a native iOS SwiftUI app and JavaScript running in a WKWebView, enabling smooth data exchange and interaction within your app.
Intro #
This post explains how to create a seamless communication bridge between a WKWebView
and a native iOS SwiftUI app, enabling smooth two-way interaction between the web view and the native application.
Swift Code #
Create a new Xcode project and replace the code in the ContentView
file with the following:
Tip: If you’re testing this in a new Xcode project, ensure you’ve selected an iPhone simulator
instead of a Mac
to avoid errors.
The Swift code below seamlessly integrates a WKWebView
into a SwiftUI application, enabling the display of web content and smooth communication between the native app and JavaScript running in the web view.
import SwiftUI
import WebKit
struct ContentView: View {
var body: some View {
WebView()
}
}
// This struct wraps a WKWebView for SwiftUI, allowing web content to be displayed within the app.
struct WebView: UIViewRepresentable {
private let scriptMessageHandlerName = "bridge"
// Creates a coordinator to manage interactions between the SwiftUI View and the WKWebView.
func makeCoordinator() -> WebViewCoordinator {
WebViewCoordinator()
}
// Configures and initializes the WKWebView.
func makeUIView(context: Context) -> WKWebView {
let userContentController = WKUserContentController()
userContentController.add(context.coordinator, name: scriptMessageHandlerName)
let configuration = WKWebViewConfiguration()
configuration.userContentController = userContentController
let wkwebview = WKWebView(frame: .zero, configuration: configuration)
wkwebview.navigationDelegate = context.coordinator
return wkwebview
}
// Loads a local HTML file into the WebView when the SwiftUI view updates.
func updateUIView(_ webView: WKWebView, context: Context) {
guard let path = Bundle.main.path(forResource: "index", ofType: "html") else { return }
let localHTMLUrl = URL(fileURLWithPath: path, isDirectory: false)
webView.loadFileURL(localHTMLUrl, allowingReadAccessTo: localHTMLUrl)
}
}
// Handles interactions between the WebView and native app code, such as navigation and script messages.
final class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
weak var webView: WKWebView?
// Stores a reference to the WebView when it finishes loading a page.
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.webView = webView
}
// Handles messages received from JavaScript in the WebView.
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
let receivedMessage = message.body as? String ?? ""
messageToWebview(msg: receivedMessage)
}
// Sends a message from the native app back to the WebView using JavaScript.
private func messageToWebview(msg: String) {
self.webView?.evaluateJavaScript("webkit.messageHandlers.bridge.onMessage('\(msg)')", completionHandler: nil)
}
}
#Preview {
ContentView()
}
HTML Code #
The HTML code defines a simple web page that communicates with the native app using JavaScript. It logs messages and sends the current timestamp to the native app when a button is clicked.
Focus on the code inside the <script>
tags.
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, height=device-height, initial-scale=1, minimum-scale=1, viewport-fit=cover">
<script>
// Logs messages to the message-log
const logMessage = (msg) => {
const p = document.createElement('p');
p.textContent = msg;
document.querySelector('#message-log').append(p);
};
// Gets the current time in HH:MM:SS format
function getTimestamp() {
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${hours}:${minutes}:${seconds}`;
}
// Listens for messages from native code
webkit.messageHandlers.bridge.onMessage = (msg) => {
logMessage('Native: Timestamp ' + msg + ' received');
};
// Sends timestamp to the native app when the button is clicked
document.addEventListener('DOMContentLoaded', () => {
document.querySelector('button').addEventListener('click', () => {
const timestamp = getTimestamp();
logMessage('WebView: Timestamp ' + timestamp + ' sent to Native');
webkit.messageHandlers.bridge.postMessage(timestamp);
});
});
</script>
</head>
<body>
<div id="message-log"></div>
<hr/>
<button>Send Timestamp</button>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
height: 100vh;
margin: 0;
background-color: #ffffff;
}
#message-log {
flex: 1;
padding: 20px;
overflow-y: auto;
}
#message-log p {
margin: 5px 0;
font-size: 16px;
}
hr {
border: 0;
border-top: 1px solid #e0e0e0;
margin: 20px 0;
}
button {
background-color: #000000;
border-style: solid;
border-color: #202020;
color: white;
padding: 15px 20px;
font-size: 20px;
border-radius: 5px;
margin: 20px;
align-self: center;
transition: background-color 0.3s;
}
button:active {
background-color: #202020;
}
</style>
</body>
</html>