How can i monitor requests on WKWebview? - javascript

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
}

Related

how to pass data from javascript to IOS Objective-c in webview?

I have a web app which i am using as Web-view app for android and IOS app.I need to pass user data to the web-view app which i achieved for android but i have no knowledge on how to do it for IOS web-view.
I have my IOS code in x-code which is in objective-c and i need to send data from frontend javascript to Objective-c, which can access it.
below is my javascript code
var myAppName = 'myfakeappname';
var myActionType = 'myJavascriptActionType';
var myActionParameters = {}; // put extra info into a dict if you need it
// (separating the actionType from parameters makes it easier to parse in ObjC.)
var jsonString = (JSON.stringify(myActionParameters));
var escapedJsonParameters = escape(jsonString);
var url = myAppName + '://' + myActionType + "#" + escapedJsonParameters;
document.location.href = url;
below is my code from Viewcontroller.m
#import "ViewController.h"
#import <WebKit/WebKit.h>
#interface ViewController ()<WKNavigationDelegate>
#property (weak, nonatomic) IBOutlet WKWebView *vwWeb;
#property (weak, nonatomic) IBOutlet UIView *vwLoading;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.vwWeb.navigationDelegate = self;
NSURL *nsurl=[NSURL URLWithString:#"http://localhost:3000"];
NSURLRequest *nsrequest=[NSURLRequest requestWithURL:nsurl];
[self.vwWeb loadRequest:nsrequest];
// Do any additional setup after loading the view, typically from a nib.
}
-(void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation{
self.vwLoading.hidden = YES;
}
//below is the code i copy pasted from various resourses i found from forums
- (BOOL)webView:( WKWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
// these need to match the values defined in your JavaScript
NSString *myAppScheme = #"myfakeappname";
NSString *myActionType = #"myJavascriptActionType";
// ignore legit webview requests so they load normally
if (![request.URL.scheme isEqualToString:myAppScheme]) {
return YES;
}
// get the action from the path
NSString *actionType = request.URL.host;
// deserialize the request JSON
NSString *jsonDictString = [request.URL.fragment stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLHostAllowedCharacterSet]];
NSLog(#"Hello, World!");
// look at the actionType and do whatever you want here
if ([actionType isEqualToString:myActionType]) {
NSLog(#"Missing function name");
// do something in response to your javascript action
// if you used an action parameters dict, deserialize and inspect it here
}
// make sure to return NO so that your webview doesn't try to load your made-up URL
return NO;
}
#end
Any help or suggestion will be greatly appreciated.
If your parameters is in the url, extract it by this property navigationAction.request.url in this function to get the params:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void)
Exactly like you started:
In JavaScript the buttons/links that need to trigger something on iOS app should do: window.location="myappname://func1";
Javascript should also have a accessible method (ie should be accessible from browser console for example) myobject.jsfunc()
On iOS WebView you intercept requests with myappname:// URL, something like:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSURL *URL = [request URL];
NSString *requestStr = [[request URL] absoluteString]; //Get the URL
//NSLog(#"Native method call '%#'", requestStr);
if ([requestStr hasPrefix:#"openpanzer"]) {
//The [URL host] is the next part of the link so we can use that like a selector
NSString *selectorName = [URL host];
id data = nil;
NSMutableArray *parameters = [NSMutableArray array];
if ( ![[URL path] isEqualToString:#""] )
{
selectorName = [NSString stringWithFormat:#"%#:", selectorName];
parameters = [NSMutableArray arrayWithArray: [[URL path] componentsSeparatedByString:#"/"] ];
[parameters removeObjectAtIndex:0]; //first object is just a slash "/"
if ( [parameters count] == 0 ) {
data = nil;
NSLog(#"NIL parameter call");
}
else if ( [parameters count] == 1 ) {
data = [parameters objectAtIndex:0];
NSLog(#"Single parameter call %#", data);
}
else {
data = parameters;
}
}
SEL method = NSSelectorFromString( selectorName );
if ([nativeGlue respondsToSelector:method]) {
if (data == nil)
[nativeGlue performSelector:method];
else
[nativeGlue performSelector:method withObject:data];
}
else {
NSLog(#"Can't find native method '%#' with param '%#'", selectorName, data);
}
return NO;
}
return YES;
}
On iOS (WebViewDelegate or AppDelegate) define your func1 ie: - (void) func1
If you need to send back results to your javascript app use:
[webView callJSFunction: jsFunc];

How do I move from my html file to another html file to create an iOS hybrid app?

I'd like to replace a webView page with a Spring-MVC structure with a hybrid format So I'd like to replace the JSP file with an HTML file and put it in the ios project folder.
This part successfully inserted the html test file, and the html load was also successful.
The problem is the current page file should be moved to other html in a file that is. Previously, the address of the page controller file was managed by Java, and the page was moved to the address value, but now the html file must be displayed.
current PageController
public class PageController {
#RequestMapping("/index")
public String index(Map<String, Object> model) {
return "/index";
}
...
current move page
function pagemove(_url) {
location.href = _url; // url: "/nextPage"
}
curerent WebViewLoad
func loadWebPage(_ webUrl : String) {
guard let myUrl = URL(string: webUrl) else {
//report invalid URL
return
}
let myRequest = URLRequest(url: myUrl)
WKWebView.load(myRequest)
}
Now I'm moving the screen in this way. How can I move if I switch to HTML?
Loading Web Views from iOS(testing)
func loadWebPage(_ webUrl : String) {
let localFilePath = Bundle.main.url(forResource: "HTMLFolder/testweb", withExtension: "html")!
let myRequest = URLRequest(url: localFilePath)
WKWebView.load(myRequest)
}
I'm using Swift5
I want to go to the testwebTwo.HTML file from the testweb.HTML file.
Thanks in advance.
Currently, your project is managed through a server built in Java. But now you have to move your page through the html file inside the ios project.
There are two ways in order to so.
Firstly, if you load a webView into an html file, your webView looks at the path to the project file.
Therefore, when you move pages, you can also specify a path. An example is an example of how html files are in the same path and created.
function pagemove(_url) {
location.href = _url; // url: "./testwebTwo.html"
}
Second, you can load page data by sending and receiving messages through js and native communication.
js function
function testMovePage() {
var data = {};
data.movePage = "HTMLFolder/testwebTwo";
try {
webkit.messageHandlers.yourMessageKey.postMessage(data);
} catch (error) {
alert(error);
}
}
Receive From Swift
#available(iOS 8.0, *)
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if message.name == "yourMessageKey" {
let sendmessage = message.body as! NSDictionary
guard sendmessage["movePage"] != nil else {
return
}
let urlString : String = sendmessage["movePage"] as! String
let filePath = Bundle.main.url(forResource: urlString, withExtension: "html")
let request = URLRequest(url: filePath)
WKWebView.load(request)
...

How to click a JavaScript button programmatically from WKWebView

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 */
}
}

Get HTML element value using WKWebKit and Swift 3

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))
}
}

userContentController never called back from JS injection

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"

Categories