In order to use a WebView in SwiftUI, we have to build an UIViewRepresentable. Let’s do it.
This below is in ViewModel.swift
import Foundation
import Combine
class ViewModel: ObservableObject {
var webViewNavigationPublisher = PassthroughSubject<WebViewNavigation, Never>()
var showWebTitle = PassthroughSubject<String, Never>()
var showLoader = PassthroughSubject<Bool, Never>()
var valuePublisher = PassthroughSubject<String, Never>()
}
// For identifiying WebView's forward and backward navigation
enum WebViewNavigation {
case backward, forward, reload
}
// For identifying what type of url should load into WebView
enum WebUrlType {
case localUrl, publicUrl
}
Now can make a coordinator which helps to communicate back and forth with the webview
class Coordinator : NSObject, WKNavigationDelegate {
var parent: WebView
var delegate: WebViewHandlerDelegate?
var valueSubscriber: AnyCancellable? = nil
var webViewNavigationSubscriber: AnyCancellable? = nil
init(_ uiWebView: WebView) {
self.parent = uiWebView
self.delegate = parent
}
deinit {
valueSubscriber?.cancel()
webViewNavigationSubscriber?.cancel()
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// Get the title of loaded webcontent
webView.evaluateJavaScript("document.title") { (response, error) in
if let error = error {
print("Error getting title")
print(error.localizedDescription)
}
guard let title = response as? String else {
return
}
self.parent.viewModel.showWebTitle.send(title)
}
/* An observer that observes 'viewModel.valuePublisher' to get value from TextField and
pass that value to web app by calling JavaScript function */
valueSubscriber = parent.viewModel.valuePublisher.receive(on: RunLoop.main).sink(receiveValue: { value in
let javascriptFunction = "valueGotFromIOS(\(value));"
webView.evaluateJavaScript(javascriptFunction) { (response, error) in
if let error = error {
print("Error calling javascript:valueGotFromIOS()")
print(error.localizedDescription)
} else {
print("Called javascript:valueGotFromIOS()")
}
}
})
// Page loaded so no need to show loader anymore
self.parent.viewModel.showLoader.send(false)
}
/* Here I implemented most of the WKWebView's delegate functions so that you can know them and
can use them in different necessary purposes */
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
// Hides loader
parent.viewModel.showLoader.send(false)
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
// Hides loader
parent.viewModel.showLoader.send(false)
}
func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
// Shows loader
parent.viewModel.showLoader.send(true)
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
// Shows loader
parent.viewModel.showLoader.send(true)
self.webViewNavigationSubscriber = self.parent.viewModel.webViewNavigationPublisher.receive(on: RunLoop.main).sink(receiveValue: { navigation in
switch navigation {
case .backward:
if webView.canGoBack {
webView.goBack()
}
case .forward:
if webView.canGoForward {
webView.goForward()
}
case .reload:
webView.reload()
}
})
}
// This function is essential for intercepting every navigation in the webview
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
// Suppose you don't want your user to go a restricted site
// Here you can get many information about new url from 'navigationAction.request.description'
if let host = navigationAction.request.url?.host {
if host == "restricted.com" {
// This cancels the navigation
decisionHandler(.cancel)
return
}
}
// This allows the navigation
decisionHandler(.allow)
}
}
To load data into the webview, below function can do
func updateUIView(_ webView: WKWebView, context: Context) {
if url == .localUrl {
// Load local website
if let url = Bundle.main.url(forResource: "LocalWebsite", withExtension: "html", subdirectory: "www") {
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
}
} else if url == .publicUrl {
// Load a public website, for example I used here google.com
if let url = URL(string: "https://www.partners.skyscanner.net/affiliates/widgets-documentation/simple-flight-search-widget") {
webView.load(URLRequest(url: url))
}
}
}
Below is how to make a WebView
func makeUIView(context: Context) -> WKWebView {
// Enable javascript in WKWebView
let preferences = WKPreferences()
preferences.javaScriptEnabled = true
let configuration = WKWebViewConfiguration()
// Here "iOSNative" is our delegate name that we pushed to the website that is being loaded
configuration.userContentController.add(self.makeCoordinator(), name: "iOSNative")
configuration.preferences = preferences
let webView = WKWebView(frame: CGRect.zero, configuration: configuration)
webView.navigationDelegate = context.coordinator
webView.allowsBackForwardNavigationGestures = true
webView.scrollView.isScrollEnabled = true
return webView
}
Now below extension can receive value from WebView
extension WebView.Coordinator: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
// Make sure that your passed delegate is called
if message.name == "iOSNative" {
if let body = message.body as? [String: Any?] {
delegate?.receivedJsonValueFromWebView(value: body)
} else if let body = message.body as? String {
delegate?.receivedStringValueFromWebView(value: body)
}
}
}
}
references:
https://blog.devgenius.io/webviews-in-swiftui-d5b1229e37ba
No comments:
Post a Comment