Skip to content

Instantly share code, notes, and snippets.

@ken-itakura
Last active November 18, 2024 05:59
Show Gist options
  • Save ken-itakura/caf891aeebaa9e3706fd99263fd038e9 to your computer and use it in GitHub Desktop.
Save ken-itakura/caf891aeebaa9e3706fd99263fd038e9 to your computer and use it in GitHub Desktop.
SwiftUI: inject javascript into a page in WKWebView, call javascript function from swift, call swift function from javascript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Example</title>
</head>
<body>
<h1>WKWebView Example</h1>
<p>HTML content loaded!</p>
</body>
</html>
//
// SampleSwiftJSInteraction.swift
//
// Created by Ken Itakura on 2024/11/17.
import SwiftUI
import WebKit
struct WebView: UIViewRepresentable {
let htmlFileName: String
@Binding var webView: WKWebView?
@Binding var callbackCount: Int
@Binding var callbackTextValue: String
class Coordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
var parent: WebView
init(_ parent: WebView) {
self.parent = parent
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
print("HTML loaded successfully")
webView.evaluateJavaScript("document.body.innerHTML") { (result, error) in
if let html = result as? String {
print("HTML content: \(html)")
} else if let error = error {
print("Error retrieving HTML content: \(error)")
}
}
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "buttonCallbackHandler" {
self.parent.callbackCount += 1 // update variable through Binding
print("JavaScript buttonCallbackHandler called: \(message.body) \(self.parent.callbackCount)")
}
else if message.name == "textFieldCallbackHandler" {
print("JavaScript textFieldCallbackHandler called: \(message.body)")
self.parent.callbackTextValue = message.body as! String // update variable through Binding
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> WKWebView {
let contentController = WKUserContentController()
let injectedFunctionScript = """
window.injectedFunctionForButton = function() {
console.log('injectedFunctionForButton called');
window.webkit.messageHandlers.buttonCallbackHandler.postMessage('Function executed!');
};
window.injectedFunctionForTexfField = function(params) {
console.log('injectedFunctionForTexfField called ' + params.textvalue);
window.webkit.messageHandlers.textFieldCallbackHandler.postMessage(params.textvalue);
};
"""
let script = """
if (document.readyState === 'complete' || document.readyState === 'interactive') {
\(injectedFunctionScript)
} else {
document.addEventListener('DOMContentLoaded', function() {
\(injectedFunctionScript)
});
}
"""
let userScript = WKUserScript(source: script, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
contentController.addUserScript(userScript)
contentController.add(context.coordinator, name: "buttonCallbackHandler")
contentController.add(context.coordinator, name: "textFieldCallbackHandler")
let config = WKWebViewConfiguration()
config.userContentController = contentController
let newWebView = WKWebView(frame: .zero, configuration: config)
newWebView.navigationDelegate = context.coordinator
#if DEBUG
newWebView.isInspectable = true
#endif
DispatchQueue.main.async {
self.webView = newWebView
}
if let htmlPath = Bundle.main.path(forResource: htmlFileName, ofType: "html") {
let htmlURL = URL(fileURLWithPath: htmlPath)
newWebView.load(URLRequest(url: htmlURL))
} else {
print("HTML file not found")
}
return newWebView
}
func updateUIView(_ uiView: WKWebView, context: Context) {}
}
struct SampleSwiftJSInteraction: View {
@State private var webView: WKWebView? = nil
@State private var callbackCount = 0
@State private var textValue = ""
@State private var callbackTextValue = ""
var body: some View {
VStack {
// SampleSwiftJSInteraction.html file must exist in project root folder (contents can be anything)
WebView(htmlFileName: "SampleSwiftJSInteraction", webView: $webView, callbackCount: $callbackCount, callbackTextValue: $callbackTextValue)
.border(Color.red)
.frame(height: 200)
Button("Tap to Call JavaScript Function") {
guard let webView = webView else {
print("WebView is not ready")
return
}
// example calling javascript function without parameter
webView.evaluateJavaScript("injectedFunctionForButton();") { result, error in
if let error = error {
print("Error: \(error)")
} else {
print("JavaScript executed successfully: \(result ?? "No result")")
}
}
}
.padding()
.border(Color.blue)
Text("Callback count: \(callbackCount)")
.padding()
TextField("Enter a value to callback", text: $textValue)
.onChange(of: textValue) { newValue in
guard let webView = webView else {
print("WebView is not ready")
return
}
// example calling javascript function with JSON object as parameter
webView.evaluateJavaScript("injectedFunctionForTexfField({textvalue: '\(newValue)'});") { result, error in
if let error = error {
print("Error: \(error)")
} else {
print("JavaScript executed successfully: \(result ?? "No result")")
}
}
}
.border(Color.blue)
HStack{
Text("Callbacked text")
Text(callbackTextValue)
}
Spacer()
}
.padding(20)
}
}
#Preview {
SampleSwiftJSInteraction()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment