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)
}
}
Related
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.
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
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)
}
}
}
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
}
I'm making an iOS application that allows 2 users interact with each other. When first user navigates to a URL on his app using UIWebView, I will send this url to the second user. The second user receive this link and use UIWebView to load it.
Everything works well until one person goes into a private zone. For example, when the first user goes to www.google.com and logs into his gmail account. Now I capture this link and send to the second user, who tries to load it, but this fails, because it's a private zone of the first user.
How can the UIWebView of second user know that the received url is in private zone or must authenticate to access it?
Here some illustration codes:
On the first user, when navigates an url like www.google.com. I call this function
- (void)loadRequestUrl:(NSString *)url {
NSLog(#"load request");
//Load request
self.websiteUrl = [url stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:websiteUrl]];
[wbWebview loadRequest:request];
//Send url to partner
//I'm using GCDAsyncSocket to send this url string to our server
//And server will send it to my current partner
//Just example code for sending
[asyncSocket send:url];
}
//On second user. I received url string from a delegate of GCDAsyncSocket. In this delegate I call this function
- (void)receiveUrl:(NSString *)url {
//Just Load request
self.websiteUrl = [url stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:websiteUrl]];
[wbWebview loadRequest:request];
}
Here an example of url that on first user after he logins his google account
https://accounts.google.com.vn/accounts/SetSID?ssdc=1&sidt=ALWU2cscH%252BVqOOjXXIRKgtz4Q8qmIcJ5lE0dy7xh7MISa%252Bw75BNSeOrF3cO91IED8Cy6PfREuDjuXLzdOMEPaaP0p6XZpCzJFQi4w2xAZa9VKubLQ5xk5%252FF%252FOj8KR0f6e5PSav%252Fww0mKEAuPoI0Dtnve600Pj6PERFtvlH3kbt2Y0hk4KEBpn6nk7zAXUdt2wc%252FaHK0%252FufzyfIMI2hkLpCFu1W1XaOIS3zwuGttA5tXjyLb3AeBLmPgfBbsd7hwZWp7IRVJGglde8gAJ%252F%252BmIhQD4eMQa1s7LD8tnuoagx%252BmRzQ4EGqtcAlE%252FGE3e8b1itkh2HXZQZYB612X1NpcPgta1XbgO7IHd0g7HsDEnsqodhHtr9F7vGl4fO%252BCcHFYaHjH3dT2mCjnOwBn%252Bbh0%252FykYpxqbx2W8K%252BHcZp3B4KI166qCPCZvgnvq7QACPsPuWGVrll4Nw2yLK%252FE2bdmFVILfgIpVbY9SheA%253D%253D&continue=http%253A%252F%252Fwww.google.com.vn%252F%253Fgfe_rd%253Dcr%2526ei%253Dgf7EU-TLL-zV8ge_rYCIAw
The second user receives it, but cannot open. I can NOT capture error on any delegate
IMPORTANCE: I don't need to force second user open it, I just want to capture the event to tell the second user that first user went into private zone
To receive and handle authentication challenge in uiwebview , here is the code.
- (void)viewDidLoad
{
// Load the web view with your url
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:#"yoururl"]]];
isauth = NO;
}
Now for the delegates. This one is called before a new request is loaded into the webview.
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
{
if (!isauth) {
isauth = NO;
[[[NSURLConnection alloc] initWithRequest:request delegate:self] autorelease];
return NO;
}
return YES;
}
//in nsurlconnections delegate. This one deals with the authentication challenge. this time set isauth to YES
- (void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge;
{
if ([challenge previousFailureCount] == 0) {
isauth = YES;
[[challenge sender] useCredential:[NSURLCredential credentialWithUser:#"username" password:#"password" persistence:NSURLCredentialPersistencePermanent] forAuthenticationChallenge:challenge];
}
else
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
// if the authentication is successfully handled than you will get data in this method in which you can reload web view with same request again.
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
{
NSLog(#"received response via nsurlconnection");
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:#"yoururl"]];
[webView loadRequest:urlRequest];
}
- (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection *)connection;
{
return NO;
}
Hope it helps.
thank you.
to get user name and password , there are multiple ways of doing it , you can create your own mechanism for the same .One simple way is.
-
(void)connection:(NSURLConnection *)connection didReceiveAuthenticationChallenge: (NSURLAuthenticationChallenge *)challenge{
NSLog(#"Need Authentication");
UIAlertView *webLogin = [[UIAlertView alloc] initWithTitle:#"Auth"
message:#"Enter User and Password"
delegate:self
cancelButtonTitle:#"Cancel"
otherButtonTitles:#"OK"
, nil];
self. challenge = challenge;
webLogin.alertViewStyle = UIAlertViewStyleLoginAndPasswordInput;
[webLogin show];
}
-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
user = [[alertView textFieldAtIndex:0]text];
pass = [[alertView textFieldAtIndex:1]text];
if (buttonIndex == [alertView cancelButtonIndex]) {
[self dismissModalViewControllerAnimated:YES];
}
else if (buttonIndex != [alertView cancelButtonIndex]) {
[self handleChallenge:self.challenge withUser:user password:pass];
}
}
- (void)handleChallenge:(NSURLAuthenticationChallenge *)aChallenge withUser:(NSString *)userName password:(NSString *)password {
NSURLCredential *credential = [[NSURLCredential alloc]
initWithUser:userName password:password
persistence:NSURLCredentialPersistenceForSession];
[[aChallenge sender] useCredential:credential forAuthenticationChallenge:aChallenge];
}