WKWebView showing white screen, with native<->webview communication - ios

Here are the functions and props inside my ViewController Class,
webViewContainer is a just a View. The page does render because it prints WEBVIEW MSG WORKING to console, but it does not appear where the View is, or it does but is just white. Removing the native-webview communication and making a direct reference from a WebView to the variable works as expected, but I need to retain the messaging functionality.
#IBOutlet weak var webViewContainer: UIView!
var webView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let contentController = WKUserContentController()
contentController.add(self, name: "callback")
let config = WKWebViewConfiguration()
config.userContentController = contentController
print(webViewContainer.frame);
webView = WKWebView(frame: webViewContainer.frame, configuration: config);
webView.loadHTMLString("<html><body><script>try {webkit.messageHandlers.callback.postMessage('WEBVIEW MSG WORKING');} catch(err) {console.log('Can not reach native code');}</script><h1>WEBVIEW SHOWS</h1></body></html>", baseURL: URL(string: "http://localhost"));
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
guard let response = message.body as? String else { return }
print(response)
}

You missed the line
self.webViewContainer.addSubview(webView)
Adding it to the end of viewDidLoad method should fix your issue.

Related

Initialize member in view struct

I want to have a WKWebView with JavaScript handling in SwiftUI. From Swift Variables Initialization, I am doing the following: (I am using https://github.com/kylehickinson/SwiftUI-WebView to provide a wrapper for WKWebView in SwiftUI that also add valuable layout constraints.)
struct ContentView: View {
var body: some View {
WebView(webView: myWebView)
.onAppear {
let url = Bundle.main.url(forResource: "index", withExtension: "html")!
myWebView.loadFileURL(url, allowingReadAccessTo: url)
let request = URLRequest(url: url)
myWebView.load(request)
}
}
class JSHandler : NSObject, WKScriptMessageHandler {
var contentView: ContentView?
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print("documentReady")
contentView!.myWebView.evaluateJavaScript("<Long Script to Transfer Data>") { (result, error) in
}
}
}
let myJSHandler = JSHandler()
var myWebView: WKWebView = {
let config = WKWebViewConfiguration()
let controller = WKUserContentController()
controller.add(myJSHandler , name: "documentReady")
config.userContentController = controller
return WKWebView(frame: .zero, configuration: config)
}()
}
But then I learned from Instance member cannot be used on type that this doesn't work because the closure doesn't have reference to self. I need the WKWebView to have dedicated config object so I can't just use the other constructor. I need a reference to it to do evaluateJavaScript.
How to make this work?
EDIT 1: Add the body and mention the framework used to wrap WKWebView.
EDIT 2: Added code to clarify that I need two way communications from WKWebView to native app via WKScriptMessageHandler (to get notified when the HTML document is ready) and from native app to WKWebView via evaluateJavaScript (to transfer data upon the HTML document is ready).
One possible solution is to configure WKWebView in init
struct ContentView: View {
private var myWebView: WKWebView
init() {
let config = WKWebViewConfiguration()
let controller = WKUserContentController()
controller.add(myJSHandler , name: "documentReady")
config.userContentController = controller
myWebView = WKWebView(frame: .zero, configuration: config)
}
var body: some View {
WebView(webView: myWebView)
.onAppear {
let url = Bundle.main.url(forResource: "index", withExtension: "html")!
myWebView.loadFileURL(url, allowingReadAccessTo: url)
let request = URLRequest(url: url)
myWebView.load(request)
}
}
class JSHandler : NSObject, WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print("documentReady")
message.webView?.evaluateJavaScript("<Long Script to Transfer Data>") { (result, error) in
}
}
}
let myJSHandler = JSHandler()
}
Just make myWebView a lazy variable, This makes sure that the jsHandler is initialised before WebView.
lazy var myWebView: WKWebView = {
let config = WKWebViewConfiguration()
let controller = WKUserContentController()
controller.add(myJSHandler , name: "documentReady")
config.userContentController = controller
return WKWebView(frame: .zero, configuration: config)
}()

How to get the Id of clicked element in a WebKit view?

I have a UIWebKit loaded in an url and I want to pick the id of a html element when clicked. I can get the element when I know the id but how to get an unknown element Id when it is clicked.
thank you for helping me !
You can do it following way.
Inject some javascript into the WebView using WKUserScript.
Injected javascript will listen to document body for any click event.
Upon an click event received, find the DOM element using elementFromPoint.
Setup that way, javascript will communicate with native code.
Upon receiving clicks and finding the element, contact native code for clicked DOM.
I've tested this way of working & it works for me.
import UIKit
import WebKit
class ViewController: UIViewController {
#IBOutlet weak var webView: WKWebView!
private var url = URL(string: "https://www.google.com")!
override func viewDidLoad() {
super.viewDidLoad()
initializeWebView()
loadData()
}
private func initializeWebView() {
let javascript = """
window.onload = function() {
document.addEventListener("click", function(evt) {
var tagClicked = document.elementFromPoint(evt.clientX, evt.clientY);
window.webkit.messageHandlers.jsMessenger.postMessage(tagClicked.outerHTML.toString());
});
}
"""
let userScript = WKUserScript.init(source: javascript,
injectionTime: .atDocumentStart, forMainFrameOnly: true)
webView.configuration.userContentController.addUserScript(userScript)
webView.configuration.userContentController.add(self, name: "jsMessenger")
}
private func loadData() {
let request = URLRequest(url: url)
webView?.load(URLRequest.init(url: url))
}
}
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print(message.body)
}
}
It gives you the whole element as string
class ViewController: UIViewController, WKNavigationDelegate, WKUIDelegate {
#IBOutlet weak var webContentView: UIView!
var web: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
let JSsource = """
window.onload = function() {
document.addEventListener("click", function(evt) {
var tagClicked = document.elementFromPoint(evt.clientX, evt.clientY);
window.webkit.messageHandlers.jsMessenger.postMessage(tagClicked.outerHTML.toString());
});
}
"""
let script = WKUserScript(source: JSsource, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
config.userContentController.addUserScript(script)
config.userContentController.add(self as! WKScriptMessageHandler, name: "jsMessenger")
self.web = WKWebView(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height-60), configuration : config)
self.web.navigationDelegate = self
self.web.uiDelegate = self
self.webContentView.addSubview(self.web!)
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
print(message.body)
}

WebView does not load

I just started developing with swift, so I am sorry if the question is basic/stupid.
I have the following setup, just a test
import WebKit
import UIKit
class ViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView!
override func loadView() {
webView = WKWebView()
webView.navigationDelegate = self
view = webView
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "https://hackingswift.com")!
webView.load(URLRequest(url:url))
webView.allowsBackForwardNavigationGestures = true
}
}
Unfortunately the browser doesn't load. The simulator only shows an empty navigation bar.
Suggestions? I am following a tutorial on hackingswift, so it's supposed to work.
You have to add webView as a subview or make an IBOutlet using Interface builder.
Try this:
class ViewController: UIViewController, WKNavigationDelegate {
var webView: WKWebView?
func loadView() {
webView = WKWebView()
webView?.navigationDelegate = self
self.view.addSubview(webView!)
}
override func viewDidLoad() {
super.viewDidLoad()
self.loadView()
let url = URL(string: "https://hackingswift.com")!
webView?.load(URLRequest(url:url))
webView?.allowsBackForwardNavigationGestures = true
}
}
If you want it a bit more simple (without nullable variable), for example:
class ViewController: UIViewController, WKNavigationDelegate {
var webView = WKWebView()
override func viewDidLoad() {
super.viewDidLoad()
webView.navigationDelegate = self
self.view.addSubview(webView)
webView?.allowsBackForwardNavigationGestures = true
self.loadUrl("https://hackingswift.com")
}
func loadUrl(_ url: String) {
if let url = URL(string: url) {
webView.load(URLRequest(url:url))
}
}
}
EDIT: it looks like some websites to load, while others do not, even if they are secure. If I put apple.com in the example, it loads, but a few others do not
Your url should be started with http or https for the webView to load.
Another possible reason is that your url containing an invalid certificate. Add the delegate function below into your code. You have to let WKWebView to bypass the certificate checking. However, this code is never recommended to go into production. You should be careful about what website your webView should and will load.
func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: #escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
let cred = URLCredential(trust: challenge.protectionSpace.serverTrust!)
completionHandler(.useCredential, cred)
}
The problem is this line:
let url = URL(string: "https://hackingswift.com")!
There is no such URL on the Internet, so you're not actually going to see anything. (You won't see anything if you paste that URL into any browser.)
So change that line to this:
let url = URL(string: "https://www.hackingwithswift.com")!
Now run the app, and presto, you'll see the web site:

ScriptMessageHandler not always called on actual device, works fine on simulator

I use WKWebView and I want to be notified when website is fully loaded. The webView:didFinishNavigation method of WKNavigationDelegate is fired when document.readyState is either interactive or complete and I want to be sure that site was completely loaded. I came up with the solution which uses JavaScript injection. Here is my MWE:
import UIKit
import WebKit
class ViewController: UIViewController, WKScriptMessageHandler, WKNavigationDelegate {
var webView: WKWebView!
#IBOutlet weak var loadLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
let contentController = WKUserContentController()
let scriptPath = NSBundle.mainBundle().pathForResource("script", ofType: "js")!
let scriptString = try! String(contentsOfFile: scriptPath)
let script = WKUserScript(source: scriptString, injectionTime: .AtDocumentStart, forMainFrameOnly: true)
contentController.addUserScript(script)
contentController.addScriptMessageHandler(self, name: "readyHandler")
let configuration = WKWebViewConfiguration()
configuration.userContentController = contentController
webView = WKWebView(frame: CGRect.zero, configuration: configuration)
webView.navigationDelegate = self
loadLabel.text = nil
}
#IBAction func loadWebsite() {
webView.loadRequest(NSURLRequest(URL: NSURL(string: "http://stackoverflow.com")!))
loadLabel.text = "Loading..."
}
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
print("message received")
loadLabel.text = "Complete"
}
}
And this is the content of script.js file:
document.onreadystatechange = function () {
if(document.readyState === "complete"){
webkit.messageHandlers.readyHandler.postMessage("");
}
}
userContentController:didReceiveScriptMessage method is always called on iOS Simulator, but on the actual device (iPhone 6 in my case) it isn't called most of the times. Any idea what can be wrong about it or what's the other way of checking if website is completely loaded?
For some reason you need to add the webView to a visible view for this to work on the device. If you don't want the webView to be visible, add it and then set the hidden property to true.
For the code example above:
func viewDidLoad(){
...
webView.hidden = true
view.addSubview(webView)
}

Send data to WKWebView

I am creating a View which displays a local webpage which also has styles and JavaScript (Essentially a hybrid native application) and I am using a WKWebView.
I know how to send data from the JavaScript to the native code but it is not clear how to do it the reverse way. I am wanting to send a JSON Object that is fetched using native iOS code to my MKWebView.
I am getting the following error
Error Domain=WKErrorDomain Code=4 "A JavaScript exception occurred" UserInfo=0x7bfb3c30 {NSLocalizedDescription=A JavaScript exception occurred}
nil
Below is my code.
//
// WebViewController.swift
//
// Created by Adam Bulmer on 09/03/2015.
// Copyright (c) 2015 Adam Bulmer. All rights reserved.
//
import Foundation
import WebKit
class WebViewController: BaseViewController, WKScriptMessageHandler, WKNavigationDelegate {
#IBOutlet var containerView : UIView! = nil
var webView: WKWebView!
override func loadView() {
super.loadView()
var config = WKWebViewConfiguration();
var contentController = WKUserContentController();
var userScript = WKUserScript(
source: "bootstrap()",
injectionTime: WKUserScriptInjectionTime.AtDocumentEnd,
forMainFrameOnly: true
)
contentController.addUserScript(userScript)
config.userContentController = contentController;
self.webView = WKWebView(frame:self.view.frame, configuration: config)
self.webView.navigationDelegate = self;
self.view = self.webView!
}
override func viewDidLoad() {
super.viewDidLoad()
var path = NSBundle.mainBundle().pathForResource(webViewName(),
ofType: "html");
var url = NSURL(fileURLWithPath: path!);
var request = NSURLRequest(URL: url!);
self.webView!.loadRequest(request);
}
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
var param = "Hello World";
var exec_template = "test(\(param)');";
self.webView!.evaluateJavaScript(exec_template, completionHandler: { (test, error) -> Void in
println(error);
println(test);
})
}
func webViewName() -> String {
return "";
}
// MARK: WKScriptMessageHandler callback
// Delegate callback for when scripts sends message
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
}
}
This code:
var param = "Hello World";
var exec_template = "test(\(param)');";
Will result in ..
test(Hello World');
.. being executed, which is not valid JS. You missed the opening ' in your code.

Resources