I need to pass some extra informations along with UIWebView loadRequest: so that it reaches my implementation of NSURLProtocol. The information cannot be bound to NSURLRequest because the information must be retained with NSURLRequest mainDocumentURL as well. So i subclassed NSURL and constructed NSURLRequest with it. I already knew that the NSURLRequest which reaches NSURLProtocol startLoading is NOT the instance i have fed to UIWebView loadRequest, so i implemented NSURL copyWithZone too, naively expecting that URL loading system will use it.
Now, NSURLProtocol canInitWithRequest is called not once as one would reasonably expect, but at least 4 times before startLoading. First 2 times of that, the incoming NSURLRequest still contains my custom NSURL implementation. Then an unfortunate internal code called CFURLCopyAbsoluteURL asks for the absoluteURL of my custom NSURL and the next canInitWithRequest (and subsequent startLoading) already gets a completely new NSURLRequest with fresh NSURL in it. copyWithZone is never called and my subclassed NSURL is lost.
Before i give up and implement an inferior and fragile solution with attaching stuff directly to the URL string, i would like to ask the wizards of higher level, whether they see a way how to catch that initial blink on the NSURLProtocol radar or how to trick CFURLCopyAbsoluteURL into carrying my custom instance. I have tried to hack NSURL absoluteURL by returning again a new instance of my custom NSURL class, but it didn't help. I have seen some promise in NSURLProtocol setProperty functionality, but now it appears pretty useless. URL loading system creates new instances of everything happily and NSURLRequest arrived in NSURLProtocol seems to be the same as the one entered into UIWebView only accidentally.
UPDATE: ok i wanted to keep the post as short as possible, but the even the first reply is asking for technical background, so here we go: i've got multiple UIWebViews in app. These views may run requests concurrently and absolutely can run requests for the same URL. It's like tabs in desktop browser. But i need to distinguish which UIWebView was the origin of each particular NSURLRequest arriving to the NSURLProtocol. I need a context being carried with each URL request. I can't simply map the URLs to data, because multiple UIWebViews may be loading the same URL at any moment.
UPDATE 2: Attaching the context information to NSURL is preferred and, as far as my understanding goes, the only usable. The issue is that requests for resources referenced inside page (images etc.) do not go through UIWebViewDelegate at all and end up in NSURLProtocol directly. I don't have a chance to touch, inspect or modify such requests anywhere prior to NSURLProtocol. The only contextual link for such requests is their NSURLRequest mainDocumentURL.
If there's some way to get your original NSURL used as mainDocumentURL that would be ideal. If there's no way to prevent it being copied, I thought of the following hack as an alternative:
Before the creation of each UIWebView, set the user agent string to a unique value. Supposedly this change only affects UIWebView objects that are created subsequently, so each view will end up with its own distinctive user agent string.
In the NSURLProtocol implementation, you can check the user agent string to identify the associated UIWebView and pass it through to the real protocol handler using the actual user agent string (so the server will see nothing different).
All this depends on the views really ending up with different UA strings. Let me know if you manage to get it to work!
You say that you can't put it on the NSURLRequest, but I'm not clear why from your updated discussion. That would be the most natural place to put it.
Implement webView:shouldLoadWithRequest:navigationType:.
Attach an extra property to the provided request using objc_setAssociatedObject. Then return YES. (It would be nice to use setProperty:forKey:inRequest: here, but UIWebView passes us a non-mutable request, so we can only attach associated objects. Yet another way that UIWebView is a pale shadow of OS X's WebView, which can handle this).
In the NSProtocol, read your extra property using objc_getAssociatedObject. The request should be the same one you were presented earlier. You suggest that this isn't the case. Are you saying that the request at webView:shouldLoadWithRequest:navigationType: is different than the request at initWithRequest:cachedResponse:client:?
Am I missing another requirement or quirk?
You can pass options through custom request headers, assuming that the targeted website or service provider don't somehow strip those in transit.
The challenge there would be coming up with an encoding scheme that can be reasonable encoded into an ASCII string for the header field value and then decoded into the actual value you want. For this, a custom NSValueTransformer would seem most appropriate.
I had the same problem. I finally stuck to the solution suggested by Matthew (using the user agent string). However, as the solution is not fleshed out, I add a new answer with more details. Furthermore, I found out that you do not need to send a request to make the user agent "stick". It is sufficient to get via javascript as suggested here.
Following steps worked for me:
(1) Get the current default user agent. You need it later to put it back into the request in NSURLProtocol. You need to use a new webview insatnce, as getting the user agent will make it stick to the webview, so you can not change it later on.
UIWebView* myWebview = [[UIWebView alloc] init];
NSString* defaultUserAgent = [myWebview stringByEvaluatingJavaScriptFromString:#"navigator.userAgent"];
[myWebview release]; // no needed with ARC, but to emphasize, that the webview instance is not needed anymore
(2) Change the value in the standardUserDefaults (taken from here).
NSDictionary* userAgentDict = [NSDictionary dictionaryWithObjectsAndKeys:#"yourUserAgent", #"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:userAgentDict];
(3) Make the new user agent string stick to your webView by getting it via javascript as done in (1), but this time on the webview instance you actually work with.
(4) Revert the default user agent in the standardUserDefaults as done here.
Related
I want to add additional fields to NSMutableURLRequest (for exapmle NSString value requestID) in order to determine correct handler for request when NSURLSession completes it.
Is it legal to create a custom NSMutableURLRequest's subclass to add specific fields? Apple documentation has no additional information about NSMutableURLRequest subclassing.
UPDATE:
I discovered that NSMutableURLRequest subclassing is not the best idea: background NSURLSession can't create download task using my custom subclass object: method downloadTaskWithRequest: always returns nil. I think this problem related with mutableCopyWithZone: that called by NSURLSession when it creates download task with request's copy.
Thanks.
I had no problem creating the task with my subclass, but when I tried to access my custom field from task.originalRequest I discovered it is a NSMutableURLRequest, not my custom subclass.
Instead of extending NSMutableRequest, I would Suggest create basic N/w call handler which would accept your custom parameter.
In this class itself you can use NSMutableRequest to create findal Request with given paramters.
This class can be used application wide to serve you response / data for any request.
I think that if Apple does not provide any warning of subclassing a class, then you can do it.
I know there are few questions similar to this one, but in all cases the answer is to make it asynchronous.
According to the apple documentation (even though it is not recommended) polling is an available option. However, I just couldn't implement it.
Should probably mention I am doing it in c# using Xamarin, but if you
can give me an answer on how to do this in Objective-C that would be
good too.
Here is my attempt
while(true)
{
if (outStream.HasSpaceAvailable())
{
int output = ((NSOutputStream)outStream).Write(data, (uint)data.Count());
}
}
The problem here is that outStream.HasSpaceAvailable() is false indefinitely.
Reason why I want to do it synchronously:
Currently (in a test app) I am doing it asynchronously and it works for sending one stream of data per call to the method. However in the real App I will need to send lots of small data packets one after the other. Therefore, I need to wait for the data to be sent before I can send another packet.
If I try putting many calls to the delegate the data keeps overwriting the previous call...
It would be great if you could let me know how to do it synchronously first (for the sake of having one answer out there on it). If you think there is a better way to do it in an async way let me know too.
Thanks
EDIT :
Here is how I set up the session
SESSION=new EASession (Accessory, Protocol);
NSStreamStatus outputStream= SESSION.OutputSream;
outputStream.Open();
This is done when a button is pressed and before the while loop above (obviously).
I have an iOS app that can download files from a website. I have created a NSURLSession in a class Downloads to manage them. The Downloads class has a NSMutableArray that keeps track of all current and past downloads using my DownloadItem objects. I am not happy with this setup.
Currently, I have to have the Downloads class as the delegate for all downloads. I see no way to assign the delegate of each NSURLSessionDownloadTask to a DownloadItem object. So, I have to keep it assigned to my Downloads object and then have it figure out which way DownloadItem to forward the message on to.
Currently I do this by making an NSMutableDictionary called tasksDictionary in the Downloads and use the taskIdentifier as a key.
return [self.tasksDictionary objectForKey:[NSNumber numberWithInteger:task.taskIdentifier]];
This seems to work, but it doesn't seem the most efficient method. I'm also concerned that I saw the first taskIdentifier created was 0 which will make it difficult to discern the difference between a completed task and the first task.
Is there a better way to keep track of these things? Is there a way to assign a new delegate for a task?
Perhaps have these ivars:
NSMutableArray *completedDownloads;
NSMutableDictionary *activeDownloadTasks; // #(taskID) => DownloadItem
When you finish, pop the DownloadItem from activeDownloadTasks and add it to completedDownloads.
Otherwise what you are doing sounds peachy.
While I'm at it, this syntax may be helpful (using your example):
return self.tasksDictionary[#(task.taskIdentifier)];
While using a 3rd party API, I have the requirement to cancel all traffic when a custom response header is set to a certain value. I am trying to find a nice place to do this check only once in my code (and not in every success/failure block, where it works fine). From what I understand, this could be done by overriding -(void)enqueueHTTPRequestOperation:(AFHTTPRequestOperation *)operation in my custom AFHTTPClient subclass, but when I implement it like that:
-(void)enqueueHTTPRequestOperation:(AFHTTPRequestOperation *)operation
{
NSLog(#"[REQUEST URL]\n%#\n", [operation.request.URL description]);
NSLog(#"[RESPONSE HEADERS]\n%#\n", [[operation.response allHeaderFields] descriptionInStringsFileFormat]);
[super enqueueHTTPRequestOperation:operation];
}
the response headers are nil. Can anybody help me with that?
At the moment when operations are being created and enqueued in AFHTTPClient, they will not have the response from the server--that will be assigned when the request operation is actually executed.
Although the requirement to cancel all traffic seems unorthodox (at least if outside of the conventions of HTTP), this is easy to accomplish:
In your AFHTTPClient subclass, add a BOOL property that stores if requests should be prevented, and then used in enqueueHTTPRequestOperation. Then, override HTTPRequestOperationWithRequest:success:failure: to execute the specified success block along with some logic to set the aforementioned property if the salient response is present.
I am making a NSURL and passing it to a selector, which then passes it to another selector, etc. By the time it gets where it's going it logs just fine, but gives a sigabort when it's used. I suspect this means my object has been released by ARC. How can I make sure it stays around long enough to get used?
__strong NSURL *url = [[NSURL alloc] initWithString:str];
... passes to a selector
... passes to another
... and then to fetchVideoContent
- (void)fetchVideoContent:(NSURL *)url withGUID:(NSString *)guid;
{
NSMutableURLRequest *req;
req = [NSMutableURLRequest requestWithURL:url // <-- DIES ON THIS LINE (SIGABRT)
cachePolicy:NSURLRequestUseProtocolCachePolicy
timeoutInterval:30.0];
...
That's the "strongest" thing I could think of and that still doesn't work. Any advice?
You need to ensure the initial url variable (__strong NSURL *url) continues to exist when the url object ends up at the fetchVideoContent method, if not, you'll get the error you're describing. Sounds to me like you're creating the url object in a method, using a local variable, and then passing that object through a few methods, that either cross to a new thread, or goes to the end of the runloop and back into the next run.
For example, if through the steps you've omitted, the current run loop ends, and the initial url variable goes out of scope, the url object will be freed, since nothing is actually holding on to it anymore. Passing the object to another method isn't enough to keep hold of it since no retain will be called on the parameter.
Short version is, make sure something holds onto url, you could make it a property of your class, an instance variable or even static if you'll only one instance of your class in use at a time.
First, you should verify that you are in fact dealing with a reference count issue -- run with zombies enabled.
I've no idea what all the URL is being passed through, but there are corner cases where explicit reference counting is required when ARC is enabled.
If MRC semantics are needed, you can use CFRetain and match that with a CFRelease, or you can create your own functions which are not compiled with ARC enabled.
Of course, you could simply use CFTypes instead (in this case).