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>

GitHub Gist