Custom http header on UIWebView requests causing back button to break - ios

I'm using the following code to inject a custom http header into my UIWebView requests:
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
let headerFields = request.allHTTPHeaderFields
var headerIsPresent = contains(request.allHTTPHeaderFields?.keys.array as [String], "X-Test-App")
if headerIsPresent || navigationType == UIWebViewNavigationType.Other {
return true
} else {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
dispatch_async(dispatch_get_main_queue(), {
let url = request.URL
var newRequest: NSMutableURLRequest = request as NSMutableURLRequest
// set new header
newRequest.addValue("MyValue", forHTTPHeaderField: "X-Test-App")
// reload the request
self.webView.loadRequest(newRequest)
})
})
return false
}
}
The problem is that the back button history is not being kept. So if I click like 3 requests deep. When hitting back, it will only go to the previous page and then hitting back again will result in it going back to the page you just went back from. It's basically an endless loop between the 2 most recent requests. Any idea how I can modify the above code to be able to keep the entire web history and allow the back/forward buttons to function as expected?

The problem is that UIWebView's built-in goBack() only goes back 1 page, but by intercepting the loadRequest and replacing it with a new request, webView is seeing this as a new, second item in the history. So more work will be required to combine 1) UIWebview built-in history, 2) UIWebView goBack(), and 3) this method of intercepting load requests.
The button calling goBack() could just call goBack() twice-- if in the same code block, I assume UIWebView would jump to the second item in the history. But this is fragile in other ways, not recommended.
You could create a stack of unmodified requests. Set up an array, and call append() to add the unmodified request before creating the new one. But first, for every unmodified request, check if it's the lastObject in the array. If so, they probably want the one before that, so remove it from the end of the array and create a modified request with the new lastObject.

Related

taskWillPerformHTTPRedirection never called in Alamofire 5

Updating Alamofire to 5.0.4. As the title says taskWillPerformHTTPRedirection is never called.
In Alamofire 4.x we could do something like:
let sessionDelegate = request.session.delegate as! Alamofire.SessionDelegate
sessionDelegate.taskWillPerformHTTPRedirection = { session, task, response, request in
if let url = task.currentRequest?.url {
// look at redirected url & act accordingly
}
}
}
A request's session/delegate has been overhauled in Alamofire 5 and is no longer directly accessible from the request. More specifically, taskWillPerformHTTPRedirection is a closure callback on ClosureEventMonitor. As a sanity check, I tested using some of the other closure callbacks.. and they worked.
// ClosureEventMonitor
let monitor = ClosureEventMonitor()
monitor.requestDidCreateTask = { request, task in
// Event fires
}
let monitor2 = ClosureEventMonitor()
monitor2.taskWillPerformHTTPRedirection = { sess, task, resp, req in
// Event Never fires
}
monitor2.requestDidFinish = { request in
// Event Fires
}
// Set up Session
var session: Session? = Session(startRequestsImmediately: false, eventMonitors: [monitor, monitor2])
let url = URL(string: "https://google.com")!
let urlRequest = URLRequest(url: url)
let trequest = session?.request(urlRequest)
For reference this code is being fired from my AppDelegate func application(_ application: UIApplication, continue userActivity: NSUserActivity for handling deep/universal links.
I'm not exactly sure what I'm missing here. Any help is greatly appreciated. Thank you for your time.
There are three things here:
First, session?.request(urlRequest) will never actually make a request, since you never call resume() (or attach a response handler).
Second, using a one off Session like that is not recommended. As soon as the Session goes out of scope all requests will be cancelled.
Third, EventMonitors cannot interact with the request pipeline, they're only observational. Instead, use Alamofire 5's new RedirectHandler protocol or Redirector type to handle redirects. There is more in our documentation. A simple implementation that customizes the action performed would be:
let redirector = Redirector(behavior: .modify { task, request, response in
// Customize behavior.
})
session?.request(urlRequest).redirect(using: redirector)

Swift: Open all external links within a webview in Safari, NOT in webview [duplicate]

This question already has answers here:
WKWebView open links from certain domain in safari
(7 answers)
Closed 5 years ago.
I wrote a simple app which calls a webview (located at XXXXXX.com/webview.php
This is my code which opens my webview in swift:
if let url = URL(string: "https://www.XXXXXX.com/webview.php") {
let request = URLRequest(url: url)
webView.loadRequest(request)
}
Now, all internal links which are within this webview-system (e.g. /webview_subsite2.php) are opening within this webview. Thats wonderful.
BUT I want that all external links (target=_blank) open in Safari-Browser.
How can I achieve this?
My previous question was marked as duplicate, but it isnt.
The point is, that the above posted code is NOT the call of a link!
It is the call of the webview.
My question concerns how to open all external links INSIDE this webview, which is located at XXXXXX.com/webview.php (domain censored) in Safari and NOT within this webview-Frame. And a solution in swift is necessary.
You can implement webView:decidePolicyForNavigationAction:decisionHandler: on your WKNavigationDelegate. Return WKNavigationActionPolicyAllow to the completion handler for URLs that match your "internal" domain, and WKNavigationActionPolicyCancel for ones that don't, and also handle those URLs to load in Safari.
use shouldStartLoadWithRequest webView delegate method and check navigationType.linkClicked type and open the link in safari
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
if navigationType == UIWebViewNavigationType.linkClicked {
if #available(iOS 10.0, *) {
UIApplication.shared.open(request.url!, options: [:])
} else {
UIApplication.shared.openURL(url)
}
return false
}
return true
}

How to find and cancel a task in NSURLSession?

I'm using an NSURLSession object to load images in my application. That could be loading several images simultaneously.
In some moments I need to cancel the loading of one specific image and continue loading others.
Could you suggest the correct way to do that?
To get tasks list you can use NSURLSession's method
- (void)getTasksWithCompletionHandler:(void (^)(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks))completionHandler;
Asynchronously calls a completion callback with all outstanding data,
upload, and download tasks in a session.
Then check task.originalRequest.URL for returned tasks to find the one you want to cancel.
Based on all the answers below, I'd go for something like this:
Swift 5
func cancelTaskWithUrl(_ url: URL) {
URLSession.shared.getAllTasks { tasks in
tasks
.filter { $0.state == .running }
.filter { $0.originalRequest?.url == url }.first?
.cancel()
}
}
You also probably want to account for your task completion handler, since canceling the task will result Error in that completion handler.
Hope below code help.
-(IBAction)cancelUpload:(id)sender {
if (_uploadTask.state == NSURLSessionTaskStateRunning) {
[_uploadTask cancel];
}
}
Swift 3.0 version of #Avt's answer to get the task list. Use getTasksWithCompletionHandler.
func getTasksWithCompletionHandler(_ completionHandler: #escaping ([URLSessionDataTask],
[URLSessionUploadTask],
[URLSessionDownloadTask]) -> Void) {
}
The returned arrays contain any tasks that you have created within the
session, not including any tasks that have been invalidated after
completing, failing, or being cancelled.
I suggest two methods:
Put the list of NSURLSessionTask in an array. In case you don't know exactly how many images you would get. Though you have to know the index of session in order to cancel it.
If you get a limited number of images. Just use a set of NSURLSessionTask as global variables so you can access to cancel it anywhere in your class.
I think you should do this...
First, keep track of your requests per xib
var download_requests = [NSURLSession]()
Then, whenever you make a request, append your request to your array like so,
let s = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration())
if let url = NSURL(string: "http://my.url.request.com")
{
download_requests.append(s)
s.dataTaskWithURL(url)
{ (data, resp, error) -> Void in
// ....
}
}
Then whenever you want to cancel any outstanding requests, (let's say on viewDidDisappear), do
override func viewDidDisappear(animated: Bool)
{
super.viewDidDisappear(animated)
//stop all download requests
for request in download_requests
{
request.invalidateAndCancel()
}
}

UIWebViewDelegate: webViewDidFinishLoad not called during in-page navigation

I have an app where users can navigate a pile of locally stored HTML files. I have a UIWebView, configured up correctly with a UIWebViewDelegate. Usually, when the user follows a link, shouldStartLoadWithRequest is called, followed by webViewDidFinishLoad a bit later.
But, if the link is pointing to an anchor on the same page as the one which is currently displayed, only shouldStartLoadWithRequest is called. webViewDidFinishLoad does not fire.
In a sense, I see that this might be expected behaviour, because in-page navigation should not require a page reload. However, I really need a place to hook into the call stack after in-page navigation is complete. The optimal solution would let me know when any sort of navigation has ended, both from another page, in-page and forward/backward actions.
My best hackaround so far has been to call performSelector: withObject: afterDelay: at the end of my shouldStartLoadWithRequest method, but I'm not happy with this.
Does anyone know how I can solve this correctly? Any insight appreciated!
You can try to use NSURLConnectionDataDelegate, it allows you to handle incoming data. Maybe you can determine if the page is loaded manually by adding a sign to your html files.
NSURLConnectionDataDelegate Reference
Edit: gist.github.com/buranmert/7304047 I wrote a piece of code and it worked, that may not be the way you wanted it to work but maybe it helps. Every time user clicks a URL with anchor, it creates another connection and as connection finishes loading web view loads the data, that marks the point where web view finished loading the page. As you use only local html files, I do not think creating connections will create problems
What you are describing is intended behavior. Just as AJAX or resource requests are never passed to the delegate, only root page changes will ever hit webViewDidFinishLoad:. But I have good news, and it doesn't involve saving a bunch of money on car insurance.
Loads performed within an iFrame DO trigger the full delegate methods and this gives you a solution. You can use this mechanism to post a notification to the native code, just as is often done for console.log() as described in this post. Alternatively, Native Bridge would work well to call into your Objective C code from JavaScript.
Just check weather u got the delegate function DidStartLoading if it is called no doubt that DidFinish also should get called
Are you sure your shouldStartLoadWithRequest always returns an YES???
I always add
return YES;
at the end of shouldStartLoadWithRequest implementation.And that works for me.
By returning YES, it denotes that the webview has loaded and would call the webViewDidFinishLoad
if([webView isLoading]==1)
{
//your webview is loading
}
else
{
//your webview has loaded
}
Here is a Swift Implementation of Mert Buran's code incase anybody is looking for it. (Although NSURLConnection is deprecated as of iOS 9)
But it does not solve my problem. When i click on a jquery link that popups a video, it does not fire the webViewDidFinishLoad.
class WebViewController: UIViewController, UIWebViewDelegate, NSURLConnectionDelegate {
var menuURL: String?
var response: NSURLResponse?
var data = NSData()
// MARK: Properties
#IBOutlet weak var webView: UIWebView!
override func viewDidLoad() {
super.viewDidLoad()
webView.delegate = self
// From Web
let url = NSURL (string: menuURL!)
let urlRequest = NSURLRequest(URL: url!)
let connection = NSURLConnection(request: urlRequest, delegate: self)
self.data = NSData()
connection!.start()
// if this is false, page will be 'zoomed in' to normal size
webView.scalesPageToFit = false
}
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
if navigationType == .LinkClicked && request.URL!.fragment != nil {
let connection = NSURLConnection(request: request, delegate: self)
connection!.start()
return false
}
return true
}
func connection(connection: NSURLConnection, didReceiveResponse response: NSURLResponse) {
self.response = response
}
func connection(connection: NSURLConnection, didReceiveData data: NSData) {
let oldData = NSMutableData(data: self.data)
oldData.appendData(data)
self.data = oldData
}
func connectionDidFinishLoading(connection: NSURLConnection) {
self.webView.loadData(self.data, MIMEType: self.response!.MIMEType!, textEncodingName: "utf-8", baseURL: self.response!.URL!)
self.data = NSData()
}
}

webVIew.request.URL in didFailLoadWithError: is the previous URL not the failed URL

Suppose there is a webpage A.html that contains a link to B.html.
If B.html is clicked then request.URL in shouldStartLoadWithRequest: will be B.html as it should be. However if there is a problem loading that page (suppose it doesn't exist for example) then in didFailLoadWithError: the value of webView.request.URL is not B.html but A.html.
Therefore it seems its not possible to know which page load failed unless I cache the last page load, but I would have expected webView.request.URL to be B.html, therefore is this a defect?
I didn't see documentation on what it should be.
[iOS 6]
I had the same problem. If anyone else does too, error.userInfo dictionary works instead.
-(void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error
{
if (error.domain == NSURLErrorDomain) {
if (error.code == NSURLErrorCancelled) { //ignore this one, interrupted load
return;
}
}
}
//NSString *theURLString = [webView.request.URL absoluteString]; this won't work - it just returns the last successful url
NSString *theURLString = [error.userInfo objectForKey:#"NSErrorFailingURLStringKey"]; //this works
The doco says NSErrorFailingURLStringKey is deprecated in iOS4 (only provided for backwards compatibility) and you should use NSURLErrorFailingURLStringErrorKey instead.
However NSURLErrorFailingURLStringErrorKey isn't returned (not by my version of UIWebView anyway). Instead, NSErrorFailingURLKey is another key returning the URL, but I can't find that in the documentation anywhere.
I faced the same issue with Swift 3.1. To get the failed url use this delegate method :
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool{
let failedUrl = (request.url?.absoluteString)! as String
return true
}
You're right that you would need to cache the last request sent (the shouldStartLoadWithRequest: delegate method would be a great place to do so), as the request property of a UIWebView appears to always return the last successful request (although the documentation doesn't specifically specify this, so I wouldn't call it a defect).

Resources