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
}
}
Related
I am trying to load a transit website so I can scrape the stop times for a given stop. After I load the url, the stop times are loaded in a bit later dynamically through javascript. My goal is to detect the presence of elements with a class of "stop-time". If these elements are present in the html I can then parse the html. But before I can parse the html I have to wait for these elements with class of "stop-time" to appear. I read through a bunch of other SO questions but I couldn't quite piece it together. I am implementing the didReceive message function but I'm not really sure how to load in the javascript I need to detect the presence of the elements (elements with class of "stop-time"). I successfully injected some javascript to prevent the location permission popup from showing.
override func viewDidLoad() {
super.viewDidLoad()
let contentController = WKUserContentController()
let scriptSource = "navigator.geolocation.getCurrentPosition = function(success, error, options) {}; navigator.geolocation.watchPosition = function(success, error, options) {}; navigator.geolocation.clearWatch = function(id) {};"
let script = WKUserScript(source: scriptSource, injectionTime: .atDocumentStart, forMainFrameOnly: true)
contentController.addUserScript(script)
let config = WKWebViewConfiguration()
config.userContentController = contentController
webView = WKWebView(frame: .zero, configuration: config)
self.view = self.webView!
loadStopTimes("https://www.website.com/stop/1000")
}
func loadStopTimes(_ busUrl: String) {
let urlString = busUrl
let url = URL(string: urlString)!
let urlRequest = URLRequest(url: url)
webView?.load(urlRequest)
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if(message.name == "stopTimesLoaded") {
// stop times now present so take the html and parse the stop times
}
}
First of all You need to inject next script to detect appearence of elements via mutations observer:
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log('mutation.type = ' + mutation.type);
for (var i = 0; i < mutation.addedNodes.length; i++) {
var node = mutation.addedNodes[i];
if (node.nodeType == Node.ELEMENT_NODE && node.className == 'stop-time') {
var content = node.textContent;
console.log(' "' + content + '" added');
window.webkit.messageHandlers.stopTimesLoaded.postMessage({ data: content });
}
}
});
});
observer.observe(document, { childList: true, subtree: true });
Then You need to subscribe for event 'stopTimesLoaded':
contentController.add(self, name: "stopTimesLoaded")
And finally add code to process data in
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage)
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))
}
}
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"
I want to call a swift function from java script code which returns the device id back to the script, I had added the code that use currently. Please advise how to return the value back to the java script from the swift function, Thanks in advance
Code Snippet :
super.viewDidLoad()
{
self.webView = WKWebView()
let preferences = WKPreferences()
preferences.javaScriptEnabled = true
let configuration = WKWebViewConfiguration()
configuration.preferences = preferences
configuration.userContentController = contentController
configuration.userContentController.addScriptMessageHandler(self,name: "interOp")
self.webView = WKWebView(frame: self.view.frame, configuration: configuration)
print(self.view.frame)
self.view = self.webView
webView!.navigationDelegate = self
let url = NSURL(string:"http://softence.com/devTest/vb_ios.html")
let req = NSURLRequest(URL:url!)
self.webView!.loadRequest(req)
}
// did receivescript message is working fine
func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage)
{
let sentData = message.body as! NSDictionary
print(sentData)
if sentData["name"] as! String == "DeviceInfo"
{
self.DeviceInfo()
}
}
// i want to return the following device info to javascript
func DeviceInfo() -> String
{
let dict = NSMutableDictionary()
dict.setValue(UIDevice.currentDevice().model, forKey: "model")
dict.setValue(UIDevice.currentDevice().name, forKey: "name")
dict.setValue(UIDevice.currentDevice().systemVersion, forKey: "system_version")
return String(dict)
}
Try having a look at the evaluateJavaScript(_:, completionHandler:) function on WKWebView as described here
To use that, your DeviceInfo function should define the complete JavaScript string which you would like to execute.
So for instance if you had a JavaScript function defined like so:
showDeviceInfo(model, name, system_version) {
}
Then your DeviceInfo function could look something like this:
func deviceInfo() {
let model = UIDevice.currentDevice().model
let name = UIDevice.currentDevice().name
let systemVersion = UIDevice.currentDevice().systemVersion
let javaScriptString = "showDeviceInfo(\(model), \(name), \(systemVersion));" //string your JavaScript string together from the previous info...and no...it aint pretty :)
webView.evaluateJavaScript(javaScriptString, completionHandler: nil)
}
You could also return javaScriptString from your DeviceInfo function and then call it in your userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage), the important things are:
you need to define the entire JavaScript string you would like to execute
you need to call evaluateJavaScript
Hope that helps you.
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
}