Reputation: 3617
Assume you have this WKWebView implementation:
import Combine
import SwiftUI
import WebKit
class WebViewData: ObservableObject {
@Published var parsedText: NSAttributedString? = nil
var isInit = false
var shouldUpdateView = true
}
struct WebView: UIViewRepresentable {
let text: String
@ObservedObject var data: WebViewData
func makeUIView(context: Context) -> WKWebView {
context.coordinator.view.navigationDelegate = context.coordinator
return context.coordinator.view
}
func updateUIView(_ uiView: WKWebView, context: Context) {
guard data.shouldUpdateView else {
data.shouldUpdateView = false
return
}
let 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" />
</head>
<body>
\(text)
<script>
let isScrolling = false;
let timer;
function toggleScrolling() {
if(!isScrolling) {
timer = setInterval(function() {
window.scrollBy(0, 1);
}, \(80 / autoScrollVelocity));
} else {
clearInterval(timer)
}
isScrolling = !isScrolling;
}
</script>
</body>
</html>
"""
uiView.loadHTMLString(html, baseURL: nil)
}
func makeCoordinator() -> WebViewCoordinator {
return WebViewCoordinator(view: self)
}
}
class WebViewCoordinator: NSObject, WKNavigationDelegate {
let view: WebView
init(view: WebView) {
self.view = view
super.init()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
DispatchQueue.main.async {
if !self.view.data.isInit {
self.view.data.isInit = true
// useless text parsing here...
}
}
}
}
in this view
import SwiftUI
struct ReadingView: View {
@ObservedObject var webViewData = WebViewData()
private let text: String
init(text: String?) {
self.text = text ?? "Sorry, this reading is empty"
}
var body: some View {
VStack {
Button("Auto scroll") {
??????
}
WebView(title: self.title, text: self.text, data: self.webViewData)
}
.onReceive(self.webViewData.$parsedText, perform: { parsedText in
if let parsedText = parsedText {
print(parsedText)
}
})
}
}
Now, in the button with label Auto scroll
, how is it possible to call the javascript inside the html toggleScrolling()
(or moving this code in a WKUserScript if necessary)? I'm pretty lost here.
Thanks in advance for any suggestion
Upvotes: 2
Views: 1814
Reputation: 52347
I'm going to address the question itself (calling evaluateJavascript
from a SwiftUI button) and not necessarily the javascript itself (your toggleScrolling
function), which I haven't tested.
I think this is a great opportunity to use Combine (which means you have to make sure to import Combine
at the top of your file) to pass messages between the views through the ObservableObject
you have set up.
Here's the final code (I had to change a few minor things about the original that wouldn't compile):
class WebViewData: ObservableObject {
@Published var parsedText: NSAttributedString? = nil
var functionCaller = PassthroughSubject<Void,Never>()
var isInit = false
var shouldUpdateView = true
}
struct WebView: UIViewRepresentable {
let text: String
@StateObject var data: WebViewData
func makeUIView(context: Context) -> WKWebView {
let webview = WKWebView()
webview.navigationDelegate = context.coordinator
return webview
}
func updateUIView(_ uiView: WKWebView, context: Context) {
guard data.shouldUpdateView else {
data.shouldUpdateView = false
return
}
context.coordinator.tieFunctionCaller(data: data)
context.coordinator.webView = uiView
let 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" />
</head>
<body>
\(text)
<script>
function doAlert() { document.body.innerHTML += "hi"; }
</script>
</body>
</html>
"""
uiView.loadHTMLString(html, baseURL: nil)
}
func makeCoordinator() -> WebViewCoordinator {
return WebViewCoordinator(view: self)
}
}
class WebViewCoordinator: NSObject, WKNavigationDelegate {
var parent: WebView
var webView: WKWebView? = nil
private var cancellable : AnyCancellable?
init(view: WebView) {
self.parent = view
super.init()
}
func tieFunctionCaller(data: WebViewData) {
cancellable = data.functionCaller.sink(receiveValue: { _ in
self.webView?.evaluateJavaScript("doAlert()")
})
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
DispatchQueue.main.async {
if !self.parent.data.isInit {
self.parent.data.isInit = true
// useless text parsing here...
}
}
}
}
struct ReadingView: View {
@StateObject var webViewData = WebViewData()
var text : String
init(text: String?) {
self.text = text ?? "Sorry, this reading is empty"
}
var body: some View {
VStack {
Button("Call javascript") {
webViewData.functionCaller.send()
}
WebView(text: text, data: webViewData)
}
.onReceive(webViewData.$parsedText, perform: { parsedText in
if let parsedText = parsedText {
print(parsedText)
}
})
}
}
There's a PassthroughSubject
on WebViewData
that doesn't take an actual value (it just takes Void
) that is used to send a signal from the SwiftUI view to the WebViewCoordinator
.
The WebViewCoordinator
subscribes to that publisher and runs evaluateJavasscript
. In order to do this, it has to have a reference to the WKWebView
, which you can see I pass along in updateUIView
You weren't actually returning a WKWebView
in makeUIView
(or maybe you were but the simplified code for the question had mangled it a bit)
Upvotes: 4