Last active
November 18, 2024 05:59
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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