WKWebView checking for HTML form submission - ios

I have been tasked with converting an Objective-C app to fully Swift 4. The app is basically a webbroswer that wraps my companies website. To keep the users form complaining about entering their username / password each time, the app needs to check when an HTML form was submitted and scrape the site and store the u/p.
In ObjC there was a method:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
That allowed me to check when a form was submitted:
static NSString * const FORM_USER = #"document.getElementById('sUserID').value";
static NSString * const FORM_PASS = #"document.getElementById('sPassword').value";
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
//save form data
if(navigationType == UIWebViewNavigationTypeFormSubmitted) {
dispatch_async(dispatch_get_main_queue(), ^{
//grab the data from the page
NSString *username = [self.webView stringByEvaluatingJavaScriptFromString: FORM_USER];
NSString *password = [self.webView stringByEvaluatingJavaScriptFromString: FORM_PASS];
//store values locally in the background
//.....
});
}
return true;
}
But I am having trouble converting that Swift 4.1 as I can't seem to find any type of UIWebViewNavigationTypeFormSubmitted for WKWebView.
Any help?
let FORM_USER = "document.getElementById('sUserID').value"
let FORM_PASS = "document.getElementById('sPassword').value"
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation) {
if(webView.url == API_LOGIN_URL){
if(WHAT A DO I CHECK FOR here?){
//Grab IN LOGIN DETAILS
var name = webView.evaluateJavaScript("\(FORM_USER)", completionHandler: nil)
var pass = webView.evaluateJavaScript("\(FORM_PASS)", completionHandler: nil)
//store values locally
//...
}
}
}

It should be
if navigationType == UIWebViewNavigationType.formSubmitted
or
if navigationType == .formSubmitted

Related

WKWebView decidePolicyForNavigationAction for html page resources

I have a need to monitor all failed requests from a given web page loaded in WKWebKit. For this, I implemented a simple controller with WKWebView on it and also conformed that controller to WKNavigationDelegate:
- (void)viewDidLoad
{
[super viewDidLoad];
webView.navigationDelegate = self;
NSURL *url = [NSURL URLWithString: #"https://google.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:URL];
[webView loadRequest:request];
}
- (void)webView:(WKWebView *)webView
decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse
decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
NSLog(#"webView.decidePolicyForNavigationResponse %#", navigationResponse.response);
decisionHandler(WKNavigationResponsePolicyAllow);
}
- (void)webView:(WKWebView *)webView
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSLog(#"webView.decidePolicyForNavigationAction %#", navigationAction.request);
decisionHandler(WKNavigationActionPolicyAllow);
}
I've also tried to implement custom NSURLProtocol extension but can see there just the initial request as with navigationDelegate:
[NSURLProtocol registerClass:[TrackingNSURLProtocol class]];
I can see my primary request to the google.com webpage in my output but the children's requests kicked off by that page are not tracked even though they are executed and downloaded by the same WKWebView when the page is loaded (css, js, images, etc).
Is it possible to achieve this kind of tracking with WKWebView (or in general with iOS)?
Depending on what "failed" means, you might prefer to implement the methods with fail in their names:
func webView(WKWebView, didFail: WKNavigation!, withError: Error)
Called when an error occurs during navigation.
func webView(WKWebView, didFailProvisionalNavigation: WKNavigation!, withError: Error)
Called when an error occurs while the web view is loading content.

WKWebView not opening custom URL scheme (js opens custom scheme link in new window)

I have a WKWebView in my application. I don't use UIWebView, because for some strange reason it doesn't open properly a web page with a lot of JS code in it.
When I tap on the link with custom url scheme "scm://", it does nothing...
My code:
- (void)viewDidLoad {
// ...
WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
if ([configuration respondsToSelector:#selector(setDataDetectorTypes:)])
[configuration setDataDetectorTypes:WKDataDetectorTypeLink];
myWebView = [[WKWebView alloc] initWithFrame:webFrame configuration:configuration];
myWebView.navigationDelegate = self;
[self.view addSubview:myWebView];
}
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
NSURL *requestURL = navigationAction.request.URL;
UIApplication *app = [UIApplication sharedApplication];
if ([requestURL.scheme.lowercaseString isEqualToString:#"scm"] && [app canOpenURL:requestURL]) {
[app openURL:requestURL];
decisionHandler(WKNavigationActionPolicyCancel);
}
else
decisionHandler(WKNavigationActionPolicyAllow);
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
}
- (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error
{
[self handleError:error];
}
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error
{
[self handleError:error];
}
#pragma mark - Handle web view errors.
- (void)handleError:(NSError *)error
{
UIApplication *app = [UIApplication sharedApplication];
app.networkActivityIndicatorVisible = NO;
NSURL *failedUrl = error.userInfo[NSURLErrorFailingURLErrorKey];
if ([failedUrl.scheme.lowercaseString isEqualToString:#"scm"]) {
[app openURL:failedUrl];
}
}
When I click custom url, handleError() is never called, neither decidePolicyForNavigationAction().
Ok, figured this out... happens that the link was opening in new window, so adding next code along with setting UIDelegate made it work
// Somewhere in viewDidLoad()...
myWebView.UIDelegate = self;
}
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures
{
NSLog(#"createWebViewWithConfiguration %# %#", navigationAction, windowFeatures);
if (!navigationAction.targetFrame.isMainFrame) {
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
[(WKWebView *)_webView loadRequest:navigationAction.request];
}
return nil;
}
-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
NSURLRequest *request = navigationAction.request;
if(![request.URL.absoluteString hasPrefix:#"http://"] && ![request.URL.absoluteString hasPrefix:#"https://"]) {
if([[UIApplication sharedApplication] canOpenURL:request.URL]) {
//urlscheme, tel, mailto, etc.
[[UIApplication sharedApplication] openURL:request.URL];
decisionHandler(WKNavigationActionPolicyCancel);
return;
}
}
decisionHandler(WKNavigationActionPolicyAllow);
}
Note: This answer is just focus on urlscheme, but maybe lead to other problems. Thanks for your feedback!
The following works in Swift 5.1 on iOS 13.1.3 (variation of #hstdt's answer) for WKWebView handling minimally the following (tested) schemas: sms:, tel:, and mailto:.
Add the following to whereever you're setting up your WKWebView.
// assign the delegate
webView.navigationDelegate = self
Then, add the following function somewhere in your class.
func webView(_ webView: WKWebView,
decidePolicyFor navigationAction: WKNavigationAction,
decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
// if the request is a non-http(s) schema, then have the UIApplication handle
// opening the request
if let url = navigationAction.request.url,
!url.absoluteString.hasPrefix("http://"),
!url.absoluteString.hasPrefix("https://"),
UIApplication.shared.canOpenURL(url) {
// have UIApplication handle the url (sms:, tel:, mailto:, ...)
UIApplication.shared.open(url, options: [:], completionHandler: nil)
// cancel the request (handled by UIApplication)
decisionHandler(.cancel)
}
else {
// allow the request
decisionHandler(.allow)
}
}
Explanation:
WKWebView seems to be unequipped to handle non-http(s) url schemas.
The above code catches the request before WKWebView has a chance to render/load it to check for a different schema.
We check for a non-http(s) schema and have the UIApplication handle it instead.
Note: Comment if you find other schemas working or not working using this solution.
Again, this solution was sourced and modified from #hstdt's answer.
Solution for Swift 5:
The following is a variation of mattblessed's answer for WKWebView which works for all application URL-Schemes not only the system URL-Schmes like "sms:", "tel:", and "mailto:"
This solution will work without adding custom URL Schemes to your apps plist file via LSApplicationQueriesSchemes. (Which is necessary since iOS 10 - for detail see this article: canOpenURL not working in ios 10)
Add the following implementation of WKNavigationDelegate to your class:
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: #escaping (WKNavigationActionPolicy) -> Void) {
if let requestUrl = navigationAction.request.url, requestUrl.isCustomUrlScheme() {
decisionHandler(.cancel)
// try to open URLs like e.g. "whatsapp://"
UIApplication.shared.open(requestUrl, options: [:]) { success in
if !success {
//TODO
// add your code for handling URLs that can't be opened here -
// maybe show an error alert
}
}
} else {
// allow the request
decisionHandler(.allow)
}
}
Add the following Extension of URL to your class:
extension URL {
func isCustomUrlScheme() -> Bool {
let webUrlPrefixes = ["http://", "https://", "about:"]
let urlStringLowerCase = self.absoluteString.lowercased()
for webUrlPrefix in webUrlPrefixes {
if urlStringLowerCase.hasPrefix(webUrlPrefix) {
return false
}
return urlStringLowerCase.contains(":")
}
}
I think this is everything you want about custom scheme ;)
https://medium.com/glose-team/custom-scheme-handling-and-wkwebview-in-ios-11-72bc5113e344
You can use setURLSchemeHandler:forURLScheme: to adds an URL scheme handler object for a given URL scheme.
If you want to add custom scheme by Swift, learn more http://samwize.com/2016/06/08/complete-guide-to-implementing-wkwebview/
This worked for me:
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
handleError(error: error as NSError)
}
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
handleError(error: error as NSError)
}
func handleError(error: NSError) {
if let failingUrl = error.userInfo[NSURLErrorFailingURLStringErrorKey] as? String {
if let url = NSURL(string: failingUrl) {
UIApplication.shared.openURL(url as URL)
}
}
}

How to get server response code for each request in WebView

I have authorization logic managed via WebView:
user click on login button in app
I open WebView with loading login webpage (opened by url address)
user provide their login and password > click on login button on page
next page is opened in WebView - authorize access confirmation page
user has to click on authorize access button on webpage
after this redirect_uri URL will be tried to be opened in WebView
I catch this redirect_uri in request and close WebView
I inspect all requests (in order to catch redirect_uri) in WebView delegate method shouldStartLoadWithRequest:.
Question: how can I manage server response for each request for page opened in WebView?
The point is that we have some problems with auth server and time to time it shows error pages. So I want to catch such pages and close WebView for such cases.
found the solution via WKWebView.
I connected it the same way as UIWebView worked in my project.
And then I actually use 2 methods from WKNavigationDelegate protocol:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
and
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
Example of code:
first method is to check whether current request is actually our redirect_uri that we should handle:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSLog(#"request: %#", navigationAction.request);
// check whether this request should be stopped - if this is redirect_uri (like user is already authorized)
if (...) {
self.webView.navigationDelegate = nil;
// do what is needed to send authorization data back
self.completionBlock(...);
// close current view controller
[self dismissViewControllerAnimated:YES completion:nil];
// stop executing current request
decisionHandler(WKNavigationActionPolicyCancel);
} else {
// otherwise allow current request
decisionHandler(WKNavigationActionPolicyAllow);
}
}
second method is to analyse response from server, and here we can verify the status code in order to handle any errors
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
// filter the responses, in order to verify only the response from specific domain
if ([[NSString stringWithFormat:[[response URL] absoluteString]] containsString:#"some.domain.com/"]) {
NSInteger statusCode = [response statusCode];
// check what is the status code
if (statusCode == 200 ||
statusCode == 301 ||
statusCode == 302) {
// allow response as everything is ok
decisionHandler(WKNavigationResponsePolicyAllow);
} else {
// handle the error (e.g. any of 4xx or any others)
self.webView.navigationDelegate = nil;
// send needed info back
self.completionBlock(...);
// close current view controller
[self dismissViewControllerAnimated:YES completion:nil];
// stop current response
decisionHandler(WKNavigationResponsePolicyCancel);
}
} else {
// current response is not from our domain, so allow it
decisionHandler(WKNavigationResponsePolicyAllow);
}
}
Implement below delegate method might be work for you.
- (void)webViewDidFinishLoad:(UIWebView *)webView {
NSCachedURLResponse *urlResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:webView.request];
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*) urlResponse.response;
NSInteger statusCode = httpResponse.statusCode;
NSLog(#"%#",statusCode);
}
In swift, you can try and cast the URLResponse to HTTPURLResponse to get the statusCode field.
extension MyController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse,
decisionHandler: #escaping (WKNavigationResponsePolicy) -> Void) {
if let httpResponse = navigationResponse.response as? HTTPURLResponse {
let statusCode = httpResponse.statusCode
//....
}
decisionHandler(.allow)
}
}

How to detect when a UIWebView has completely finished loading?

I am trying to build a filter for a UIWebView and I am struggiling to detect when the UIWebView has completely finished loading. I have used the following two methods
– webView:shouldStartLoadWithRequest:navigationType:
– webViewDidFinishLoad:
but the issue is that these will be called multiple times when a page has frames and additional content to load.
What I need is to know when the view has completely loaded and there is no more content to fetch. Then when the content has loaded I can check to URL of the page against a list of approved URLS.
ANy ideas?
Use the UIWebViewDelegate protocol method webViewDidFinishLoad and webView's isLoading property
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
//Check here if still webview is loding the content
if (webView.isLoading)
return;
//after code when webview finishes
NSLog(#"Webview loding finished");
}
Swift 3 version:
func webViewDidFinishLoad(_ webView: UIWebView) {
if webView.isLoading{
return
}
print("Done loading")
}
Try use:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)requestURL navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = [requestURL URL];
NSLog(#"##### url = %#",[url absoluteString]);
return YES;
}
don't forget to set your UIWebView Delegate
or add statement,
NSRange range = [[url absoluteString] rangeOfString:#"https://www.google.com"];
if (range.location != NSNotFound)
{}
hope to help you.
It is true that the original question was posted many years ago. Recently I had to find a reliable solution to this issue.
Here is the solution that worked for me:
Replaced UIWebView with WKWebWiew.
Added 'evaluateJavaScript' code while handling the 'didFinishNavigation' delegate method.
The complete code is:
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation{
[webView evaluateJavaScript:#"document.body.innerHTML" completionHandler:^(id result, NSError *error) {
if (result != nil) {
// Call your method here
}
if(error) {
NSLog(#"evaluateJavaScript error : %#", error.localizedDescription);
}
}];
}
If you aren't an owner of UIWebView and as a result you don't have an access to webViewDidFinishLoad but you have a reference to UIWebView you can use the next code
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { (timer) in
guard uiWebView.isLoading else {
return
}
timer.invalidate()
//Add your logic here
}

Delegate to know when phone call ends which was initiated by app

I have a code which places a phone call using below code :
// Make a call to given phone number
- (void)callPhoneNumber:(NSString *)phoneNumber
{
if (!self.webview)
self.webview = [[UIWebView alloc] init];
self.webview.delegate = self;
// Remove non-digits from phone number
phoneNumber = [[phoneNumber componentsSeparatedByCharactersInSet:[[NSCharacterSet decimalDigitCharacterSet] invertedSet]] componentsJoinedByString:#""];
// Make a call
NSURL * url = [NSURL URLWithString:[NSString stringWithFormat:#"tel:%#", phoneNumber]];
[self.webview loadRequest:[NSURLRequest requestWithURL:url]];
[self.view addSubview:self.webview];
}
This makes a call. What I want is, I want to know when user ends a call. I have to perform an operation when user ends a call. Is there any way for it?
What I tried is, I set delegate of webview to current controller. But none of the delegate methods is called.
- (void)webViewDidStartLoad:(UIWebView *)webView
{
DLog(#"Start Loading");
}
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
DLog(#"Finish Loading");
}
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
DLog(#"Did fail with error : %#", error);
}
I don't know if you need this info but I use webview so that when phone call is made, flow remains within the app and on call end, app screen is displayed rather than user manually coming to app from native contact app.
CoreTelephony framework has a CTCallCenter Class with callEventHandler property.
#property (nonatomic, copy) void (^callEventHandler)(CTCall*);
You will have to define a handler block in your application and assign it to this property.
If your application is active when a call event takes place, the system dispatches the event to your handler immediately when call state changes. Refer apple documents found here.
I used below code to get notified of call events.
// Create CTCallCenter object
callCenter = [[CTCallCenter alloc] init];
// Assign event handler. This will be called on each call event
self.callCenter.callEventHandler = ^(CTCall* call) {
// If call ended
if (call.callState == CTCallStateDisconnected)
{
NSLog(#"Call ended.");
}
};
For other call states check out Call States. For more info on CTCallCenter look at Apple doc for CTCallCenter.
You should be implementing this delegate method of UIWebView
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
on end call operation your webview will notify this delegate method about your action perform and you can handle it in there for example
-(BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
if (navigationType == UIWebViewNavigationTypeLinkClicked )
{
NSURL *url = [request URL];
NSLog(#"URL ===== %#",url);
// you can check your end call operation URL and handle it accordingly
if ([[url absoluteString] rangeOfString:#"#"].location == NSNotFound)
{
[[UIApplication sharedApplication] openURL:[request URL]];
return NO;
}
//return NO;
}
return YES;
}

Resources