Trying to get an html element name or value using preferred evaluate Javascript swift function using a WKWebView. I keep recieving nil. I think maybe my function is firing before the page is loaded, thus never finding the element. Right now just trying to get the element text by class name. Here is my code below in the ViewController
import UIKit
import WebKit
class ViewController: UIViewController, WKUIDelegate, WKNavigationDelegate {
var web: WKWebView!
override func loadView() {
let webConfiguration = WKWebViewConfiguration()
web = WKWebView(frame: .zero, configuration: webConfiguration)
web.uiDelegate = self
view = web
}
override func viewDidLoad() {
super.viewDidLoad()
let url = URL(string: "http://www.appcoda.com")
let myRequest = URLRequest(url: url!)
web.load(myRequest)
web.evaluateJavaScript("document.getElementByClassName('excerpt').innerText") {(result, error) in
if error != nil {
print(String(describing:result))
}
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
}
You are partially right. The website won't finish loading by the time you call evaluateJavascript. But there are also other errors:
You should set the navigationDelegate instead of uiDelegate
Due to App Transport Security, URL requests to http addresses are disallowed. Use https if the website supports it or configure your app appropriately.
You misspelled the function name (missing the the s after getElement)
getElementsByClassName returns an array so you have to pick what element you want to find the innerText of
Try this:
override func viewDidLoad() {
super.viewDidLoad()
web = WKWebView(frame: .zero)
web.navigationDelegate = self
let url = URL(string: "https://www.appcoda.com")
let myRequest = URLRequest(url: url!)
web.load(myRequest)
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
web.evaluateJavaScript("document.getElementsByClassName('excerpt')[0].innerText") {(result, error) in
guard error == nil {
print(error!)
return
}
print(String(describing: result))
}
}
Related
I need my swift class to interact with the html and javascript in a wkwebview that it references, in particular, feed it a variable.
I thought I would start simply by trying to get the webview to retrieve something using getElementByID following the code here. However, it is not working.
Here is code:
HTML file
<div id="username">#twostraws</div>
Swift File
let webView = WKWebView()
override func viewDidLoad() {
super.viewDidLoad()
if let url = Bundle.main.url(forResource: "myHTML", withExtension: "html") {
webView.loadFileURL(url, allowingReadAccessTo: url.deletingLastPathComponent())
}
webView.evaluateJavaScript("document.getElementById('username').innerText") { (result, error) in
if let result = result {
print("what is in div",result)
}
else {
print("didn't find it")
}
}
override func loadView() {
self.view = webView
}
The webview is loading, however nothing is getting retrieved by evaluateJavascript.
Edit:
This is error message:
A JavaScript exception occurred" UserInfo={WKJavaScriptExceptionLineNumber=1, WKJavaScriptExceptionMessage=TypeError: null is not an object (evaluating 'document.getElementById('username').innerText'), WKJavaScriptExceptionColumnNumber=32, WKJavaScriptExceptionSourceURL=about:blank, NSLocalizedDescription=A JavaScript exception occurred})
Thanks for any suggestions.
I'm trying to get WKWebView to load this calendar page (csbcsaints.org/calendar) and immediately go to the next month by clicking that arrow button. Following along with WWDC 2014's 'Introducing the Modern WebKit API,' here's what I have so far:
class WebViewController: UIViewController {
let jsString = "document.getElementById('evcal_next').click()"
override func viewDidLoad() {
super.viewDidLoad()
let script = WKUserScript(source: jsString, injectionTime: .atDocumentEnd, forMainFrameOnly: true)
let contentController = WKUserContentController()
contentController.addUserScript(script)
let myConfiguration = WKWebViewConfiguration()
myConfiguration.userContentController = contentController
myConfiguration.preferences.javaScriptEnabled = true
let webView = WKWebView(frame: .zero, configuration: myConfiguration)
self.view = webView
if let url = URL(string: "https://csbcsaints.org/calendar") {
let request = URLRequest(url: url)
webView.load(request)
}
}
}
When that didn't work with either .atDocumentStart or .atDocumentEnd, I tried calling webView.evaluateJavaScript(jsString) after loading the page instead, but it only returned an error:
Error Domain=WKErrorDomain Code=4
"A JavaScript exception occurred"
UserInfo={
WKJavaScriptExceptionLineNumber=1,
WKJavaScriptExceptionMessage=
TypeError: null is not an object (evaluating 'document.getElementById('evcal_next').click'),
WKJavaScriptExceptionColumnNumber=38,
WKJavaScriptExceptionSourceURL=about:blank,
NSLocalizedDescription=A JavaScript exception occurred
}
It seems to say that the command I passed was invalid, but I'm not sure, since running jsString in Safari's console performed the effect I wanted. I'm relatively experienced with Swift and iOS, but new to JavaScript, so any help would be appreciated.
You should assign the delegate of the webView like:
webView.delegate = self
and do it inside didEnd... method:
extension WebViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
/* This is where you can perform DOM with JS in webview */
}
}
This is sums up all iOS developing i have ever done. So I'm very new to swift or any iOS developing for that matter.
What I'm trying to accomplish is something that behaves almost like jsonp when doing cross domain ajax request in javascript (if you know how that works...)
I have a problem in that I can't evaluate javascript and get back something that behaves is asynchronous. For example getting anything from the indexedDB in the webview
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after lvaring the view, typically from a nib.
self.webview = WKWebView()
self.i = 0
let preferences = WKPreferences()
let configuration = WKWebViewConfiguration()
preferences.javaScriptEnabled = true
configuration.preferences = preferences
configuration.userContentController.add(self as WKScriptMessageHandler,name: "bridge")
self.webview = WKWebView(frame: self.view.frame, configuration: configuration)
webview.navigationDelegate = self
webview.isHidden = true
webview.load(URLRequest(url: URL(string: "http://example.com")!))
/*
help...
don't know how closure & completion Handler (callbacks) works in swift
runjs("Promise.resolve({msg: 'hello world'})", completionHandler: (result) -> Void) {
print(result) // expecting it to be {msg: 'hello world'}
})
*/
}
My helper function
func runjs(input: String, completeHandler: Any) {
self.i = self.i + 1
// TODO: store the completeHandler in a some HashMap with `i` as the key
let js = "Promise.resolve(" + input + ").then(res => webkit.messageHandlers.bridge.postMessage({i: " + String(self.i) + ", data: res}))"
webview.evaluateJavaScript(js)
}
My message handler
/// Adds support for the WKScriptMessageHandler to ViewController.
extension ViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) {
let json:NSDictionary = message.body as! NSDictionary
let i:Int = json["i"] as! Int
let data:NSDictionary = json["data"] as! NSDictionary
// TODO: get the complete handler from a HashMap[i]
// TODO: call the complete handler with data as argument
// TODO: remove the complete handler from the HashMap
}
}
I've got a WBWebview running into a NSPopover.
I've followed this guide in order to be able to send data back to my Swift application from JS.
Unfortunately, userContentController is never called in the Swift app.
Here's my Swift app View controller code:
class GSViewController: NSViewController, WKScriptMessageHandler, WKNavigationDelegate {
var webView: WKWebView?
var webConfig:WKWebViewConfiguration {
get {
// Create WKWebViewConfiguration instance
let webCfg:WKWebViewConfiguration = WKWebViewConfiguration()
// Setup WKUserContentController instance for injecting user script
let userController:WKUserContentController = WKUserContentController()
// Add a script message handler for receiving "buttonClicked" event notifications posted from the JS document using window.webkit.messageHandlers.buttonClicked.postMessage script message
userController.addScriptMessageHandler(self, name: "buttonClicked")
// Get script that's to be injected into the document
let js:String = buttonClickEventTriggeredScriptToAddToDocument()
// Specify when and where and what user script needs to be injected into the web document
let userScript:WKUserScript = WKUserScript(source: js, injectionTime: WKUserScriptInjectionTime.AtDocumentEnd, forMainFrameOnly: false)
// Add the user script to the WKUserContentController instance
userController.addUserScript(userScript)
// Configure the WKWebViewConfiguration instance with the WKUserContentController
webCfg.userContentController = userController;
return webCfg;
}
}
override func loadView() {
super.loadView()
}
override func viewDidLoad() {
super.viewDidLoad()
self.webView = WKWebView(frame: self.view.frame, configuration: webConfig)
self.webView!.navigationDelegate = self
self.view = webView!
let username = NSUserName()
let url = NSURL(string:"someUrl")
let req = NSURLRequest(URL:url!)
self.webView!.loadRequest(req)
self.view.frame = CGRectMake(0, 0, 440, 600)
}
func buttonClickEventTriggeredScriptToAddToDocument() ->String{
let script:String = "webkit.messageHandlers.callbackHandler.postMessage('Does it works?');"
return script;
}
// WKNavigationDelegate
func webView(webView: WKWebView, didFinishNavigation navigation: WKNavigation!) {
NSLog("%s", #function)
}
func webView(webView: WKWebView, didFailNavigation navigation: WKNavigation!, withError error: NSError) {
NSLog("%s. With Error %#", #function, error)
showAlertWithMessage("Failed to load file with error \(error.localizedDescription)!")
}
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
print("called")
if(message.name == "callbackHandler") {
print("It does ! \(message.body)")
}
}
// Helper
func showAlertWithMessage(message:String) {
let myPopup:NSAlert = NSAlert()
myPopup.messageText = message
myPopup.alertStyle = NSAlertStyle.WarningAlertStyle
myPopup.addButtonWithTitle("OK")
myPopup.runModal()
}
}
I'm trying to inject directly the call to Swift callback webkit.messageHandlers.callbackHandler.postMessage('Does it works?');, but it seems not to work.
With different test, I've discovered that the injection actually works in the Swift -> JS way (I've been able to modify CSS of visible HTML elements via injection of JQuery code), but the JS -> Swift bridge just doesn't seem to make it.
Does anyone have an idea why?
I'm under OSX 10.11.6, running XCode 7.3.6.
I'm really new into Swift and OSX programming in general, so doesn't hesitate to point out any element I'm missing. I'm voluntary omitting the JS my page uses, since I'm not using any of it in the example.
Thanks.
Found it, was totally my bad.
I forgot to rename the message handler when I added my script to the webview configuration.
userController.addScriptMessageHandler(self, name: "callbackHandler") // was originally "ButtonClicked"
How can i monitor requests on WKWebview?
I'v tried using NSURLprotocol (canInitWithRequest) but it won't monitor ajax requests (XHR), only navigation requests(document requests)
Finally I solved it
Since I don't have control over the web view content, I injected to the WKWebview a java script that include a jQuery AJAX request listener.
When the listener catches a request it sends the native app the request body in the method:
webkit.messageHandlers.callbackHandler.postMessage(data);
The native app catches the message in a delegate called:
(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
and perform the corresponding actions
here is the relevant code:
ajaxHandler.js -
//Every time an Ajax call is being invoked the listener will recognize it and will call the native app with the request details
$( document ).ajaxSend(function( event, request, settings ) {
callNativeApp (settings.data);
});
function callNativeApp (data) {
try {
webkit.messageHandlers.callbackHandler.postMessage(data);
}
catch(err) {
console.log('The native context does not exist yet');
}
}
My ViewController delegate are:
#interface BrowserViewController : UIViewController <UIWebViewDelegate, WKUIDelegate, WKNavigationDelegate, WKScriptMessageHandler, UIWebViewDelegate>
And in my viewDidLoad(), I'm creating a WKWebView:
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc]init];
[self addUserScriptToUserContentController:configuration.userContentController];
appWebView = [[WKWebView alloc]initWithFrame:self.view.frame configuration:configuration];
appWebView.UIDelegate = self;
appWebView.navigationDelegate = self;
[appWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString: #"http://#############"]]];
Here is the addUserScriptToUserContentController:
- (void) addUserScriptToUserContentController:(WKUserContentController *) userContentController{
NSString *jsHandler = [NSString stringWithContentsOfURL:[[NSBundle mainBundle]URLForResource:#"ajaxHandler" withExtension:#"js"] encoding:NSUTF8StringEncoding error:NULL];
WKUserScript *ajaxHandler = [[WKUserScript alloc]initWithSource:jsHandler injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
[userContentController addScriptMessageHandler:self name:#"callbackHandler"];
[userContentController addUserScript:ajaxHandler];
}
#Benzi Heler answer is great, but it uses jQuery which seems like is not working in WKWebView anymore, so I have found solution without using jQuery.
Here is ViewController implementation that lets you be notified every AJAX request is completed in WKWebView:
import UIKit
import WebKit
class WebViewController: UIViewController {
private var wkWebView: WKWebView!
private let handler = "handler"
override func viewDidLoad() {
super.viewDidLoad()
let config = WKWebViewConfiguration()
let userScript = WKUserScript(source: getScript(), injectionTime: .atDocumentStart, forMainFrameOnly: false)
config.userContentController.addUserScript(userScript)
config.userContentController.add(self, name: handler)
wkWebView = WKWebView(frame: view.bounds, configuration: config)
view.addSubview(wkWebView)
if let url = URL(string: "YOUR AJAX WEBSITE") {
wkWebView.load(URLRequest(url: url))
} else {
print("Wrong URL!")
}
}
private func getScript() -> String {
if let filepath = Bundle.main.path(forResource: "script", ofType: "js") {
do {
return try String(contentsOfFile: filepath)
} catch {
print(error)
}
} else {
print("script.js not found!")
}
return ""
}
}
extension WebViewController: WKScriptMessageHandler {
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let dict = message.body as? Dictionary<String, AnyObject>, let status = dict["status"] as? Int, let responseUrl = dict["responseURL"] as? String {
print(status)
print(responseUrl)
}
}
}
Pretty standard implementation. There is a WKWebView created programmatically. There is injected script that is loaded from script.js file.
And the most important part is script.js file:
var open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function() {
this.addEventListener("load", function() {
var message = {"status" : this.status, "responseURL" : this.responseURL}
webkit.messageHandlers.handler.postMessage(message);
});
open.apply(this, arguments);
};
userContentController delegate method will be called every time there is AJAX request loaded. I'm passing there status and responseURL, because this was what I needed in my case, but you can also get more informations about request. Here is the list of all properties and methods available:
https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
My solution is inspired by this answer written by #John Culviner:
https://stackoverflow.com/a/27363569/3448282
If you have control of the content inside the WkWebView you can send messages to your native app using window.webkit.messageHandlers whenever you make an ajax request, which will be received as a WKScriptMessage that can be processed by whatever you've designated as your WKScriptMessageHandler. The messages can contain whatever information you wish, and will be automatically converted into native objects/values in your Objective-C or Swift code.
If you don't have control over the content you can still do this by injecting your own JavaScript via a WKUserScript to track ajax requests and send back messages using the method stated above.
You can use this to respond to requests from the WKWebView. It works similar to UIWebView.
- (void)webView:(WKWebView *)webView2 decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (navigationAction.navigationType == WKNavigationTypeLinkActivated) {
NSString *url = [navigationAction.request.URL absoluteString];
// Handle URL request internally
}
decisionHandler(WKNavigationActionPolicyAllow); // Will continue processing request
decisionHandler(WKNavigationActionPolicyCancel); // Cancels request
}