I'm currently trying to log all URL connections made in my app. Is there a debug switch, or network activity monitor tool I can use to do this?
Or is my only alternative to include NSLog statements throughout the source base?
Thanks,
Ash
All NSURLConnections of your application use a shared cache class for default
http://developer.apple.com/library/mac/#documentation/Cocoa/Reference/Foundation/Classes/NSURLCache_Class/Reference/Reference.html
So, one thing you could do is subclass the default cache, and then at the cachedResponseForRequest NSURLCache method, you can to track you requests.
#interface CustomNSURLCache : NSURLCache {
}
#end
#implementation CustomNSURLCache
-(NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
NSLog(#"connection will send request for url: %#", request);
return [super cachedResponseForRequest:request];
}
#end
At your AppDelegate didFinishLaunchingWithOptionsmethod, set the shared cache to an instance of your cache.
CustomNSURLCache *customCache = [[CustomNSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:51200 diskPath:nil];
[NSURLCache setSharedURLCache:customCache];
[customCache release];
(being 0 the default value for MemoryCapacity and 512000 for DiskCapacity)
Now when you create a new connection
NSURLRequest *request1 = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:#"http://stackoverflow.com"]];
[[NSURLConnection alloc] initWithRequest:request1 delegate:self];
you should see something like this at your console
connection will send request for url: <NSURLRequest https://stackoverflow.com/>
In Instruments there is a Network Activity Monitor under the System Instruments. I have not personally used it though so I don't know if it does what you want.
Related
I have seen several posts for this question and I cant figure out how to get this working. For example, this post NSCachedURLResponse returns object, but UIWebView does not interprets content
suggests that we subclass and override cachedResponseForRequest and modify the NSURLRequest before returning a cached response (not sure why this needs to be done).
This is the code i have in my NSURLCache subclass:
- (NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request {
NSCachedURLResponse *resp = [super cachedResponseForRequest:request];
if (resp != nil && resp.data.length > 0) {
NSDictionary *headers = #{#"Access-Control-Allow-Origin" : #"*", #"Access-Control-Allow-Headers" : #"Content-Type"};
NSHTTPURLResponse *urlresponse = [[NSHTTPURLResponse alloc] initWithURL:request.URL statusCode:200 HTTPVersion:#"1.1" headerFields:headers];
NSCachedURLResponse *cachedResponse = [[NSCachedURLResponse alloc] initWithResponse:urlresponse data:resp.data];
return cachedResponse;
} else {
return nil;
}
}
Now when i load the request via the webview and I'm offline, the delegate didFailLoadWithError gets called (it was not able to load), but before that delegate method is called, my cachedResponseForRequest method gets called and the cached response has some html however my didFailLoadWithError method still gets called. So my question is, should the webViewDidFinishLoad should get called if we return the cached response from the NSURLCache, if not how do i make this work?
I have this in my didFinishLaunchingWithOptions:
NSUInteger cacheSizeMemory = 500*1024*1024; // 500 MB
NSUInteger cacheSizeDisk = 500*1024*1024; // 500 MB
NSURLCache *sharedCache = [[CustomCache alloc] initWithMemoryCapacity:cacheSizeMemory diskCapacity:cacheSizeDisk diskPath:#"nsurlcache"];
[NSURLCache setSharedURLCache:sharedCache];
This is the flow i want working:
Launch the app.
Hit some url (lets say slickdeals.net)
Go offline kill the application
Launch the application
UIWebView attempts to hit url but device is offline and a returned cached response is returned.
We display cached response.
Any thoughts on what needs to change in my code is appreciated!
NSURLCache is designed for HTTP conform caching, and often behaves unexpectly. You must wrote your own cache for your use case, and implement a custom NSURLProtocol to use it. The protocol put the responses into your cache, and provides them from cache when offline. Rob Napier wrotes an excellent article about this.
I am using a Master Detail Controller. In the Master list there are 5 items. On selecting each item, there are Asynchronous calls made.
There is one SingleTon class, which handles all network calls.
[[MyNetwokCommunication connectionInstance]
makeRequest:"url1" delegate:self];
[[MyNetwokCommunication connectionInstance] makeRequest:"url2" delegate:self];
Actual implementation in makeRequest:delegate: is [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediatley:YES].
So, is this a correct way to handle network connection with a singleton class, as it might over-ride data part of one request with another.
There are a lot of ways to handle it. Unfortunately, the fact that NSURLConnection can't be used as a key in a dictionary makes life particularly miserable.
The best thing to do, assuming you don't still need to support iOS 6 and earlier (or OS X v10.8 or earlier) is to ditch NSURLConnection for NSURLSession. Then use block-based calls to make the request and handle the response. Unlike NSURLConnection objects, you can use NSURLSessionTask objects as dictionary keys, which makes it easy to keep track of what data came from which request, and to store additional objects or other data associated with that request (e.g. storing the UI cell where the data should go).
If you have to use NSURLConnection to support older operating systems, you might consider subclassing NSURLRequest and adding extra data. Note that this alternative approach does not work with NSURLSession, and you'll need to provide your own redirect handler, because otherwise you'll end up getting back generic NSURLRequest objects whenever a redirect happens, IIRC. Or you can add Objective-C associated objects on the connection object. (At least I'm 99% sure that this works.)
That's not how I would do it.
The best paradigm on iOS to serialize things is an NSOperationQueue. You can create a queue with concurrency of 1, then queue your NSURLConnection or NSURLSession children as NSOperations.
This allows you to do neat things like create operations that are dependent on the success of other operations.
Here's the creation of the queue:
#property (strong, nonatomic) NSOperationQueue *queue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_queue = [[NSOperationQueue alloc] init];
[_queue setMaxConcurrentOperationCount:1];
});
Then to create a network operation:
// HTTPOperation.h
#import <Foundation/Foundation.h>
#interface HTTPOperation : NSOperation
#end
//
// HTTPOperation.m
//
#import "HTTPOperation.h"
#implementation HTTPOperation
- (void) main
{
NSURLRequest * urlRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://google.com"]];
NSURLResponse * response = nil;
NSError * error = nil;
NSData * data = [NSURLConnection sendSynchronousRequest:urlRequest
returningResponse:&response
error:&error];
if (error == nil)
{
// Parse data here
NSLog(#"Completed successfully");
}
}
#end
To execute the operations:
- (IBAction)queueHTTP {
HTTPOperation *op = [HTTPOperation new];
[self.queue addOperation:op];
}
You can queue as many as you want from anywhere and they will execute serially. I recommend watching the WWDC video on Advanced NSOperations:
https://developer.apple.com/videos/wwdc/2015/?id=226
It was very informative.
I initialized NSURLCache in my app delegate like so:
int cacheSizeMemory = 4*1024*1024; // 4MB
int cacheSizeDisk = 32*1024*1024; // 32MB
MyServerCache *sharedCache = [[MyServerCache alloc] initWithMemoryCapacity:cacheSizeMemory diskCapacity:cacheSizeDisk diskPath:#"nsurlcache"];
[MyServerCache setSharedURLCache:sharedCache];
Then I create a request with default cache policy and call:
[NSURLConnection sendSynchronousRequest: urlRequest
returningResponse: &response
error: pError ];
The response is of type "application/json" and has "Cache-Control" set to "private, max-age=600". The response is only 874 bytes (shouldn't be too large to cache), however NSURLCache does not seem to work.
I have run this on a simulator and looked for the on-disk cache which doesn't seem to exist, so I subclassed NSURLCache (MyServerCache) and overrode the following methods to see if they were being called:
-(NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request
{
NSLog(#"Returning cached response for request: %#", request);
return [super cachedResponseForRequest:request];
}
-(void)storeCachedResponse:(NSCachedURLResponse *)cachedResponse forRequest:(NSURLRequest *)request
{
NSLog(#"Caching response for request: %#", request);
[super storeCachedResponse:cachedResponse forRequest:request];
}
No matter what I do (and I've tried several different API's and responses), NSURLCache never seems to cache anything. I can never find the on-disk cache in the file system, and the methods above are never called on my subclass of NSURLCache (MyServerCache).
I'd really like to use this rather than rolling my own cache, but I have no idea why it's not working or how to get it to work. Please help.
You have to create your request with the NSURLRequestUseProtocolCachePolicy and it works perfectly out-of-the box with NSURLCache.
NSURLRequestCachePolicy cachePolicy = NSURLRequestUseProtocolCachePolicy;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url
cachePolicy:cachePolicy
You can read about the NSURLRequestCachePolicy here: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSURLRequest_Class/#//apple_ref/c/tdef/NSURLRequestCachePolicy
A little bit confusing ist that iOS always returns a 200 - Success so you will never get a 304 - Resource not Modified when a response is coming from cache.
So it´s more easy to see the cache policy working when monitoring the server side.
You can use https://github.com/PaulEhrhardt/Simple-Cache-Test to validate cache-control, expires and ETag with a little local ruby server. It´s just a few lines to set-up. I tested both successfully with 7.1 and 8.4 iOS SDK.
We have set up a simple NSURLConnection and NSURLCache as per the abbreviated code snippet below. We have made sure, that the server (localhost:9615) returns the following caching headers:
ETag : abcdefgh
Cache-Control : public, max-age=60
However, the willCacheResponse delegate method is never called. Any idea?
Code:
// In the app delegate
NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil];
[NSURLCache setSharedURLCache:URLCache];
// Triggered by a UIButton
- (IBAction)sendRequest:(UIButton *)sender {
NSLog(#"sendRequest");
NSURLRequest *theRequest=[NSURLRequest requestWithURL:[NSURL URLWithString:#"http://localhost:9615"] cachePolicy:NSURLRequestReturnCacheDataElseLoad timeoutInterval:60.0];
NSMutableData *receivedData = [NSMutableData dataWithCapacity: 0];
NSURLConnection *theConnection=[[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if (!theConnection) {
receivedData = nil;
}
}
- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse
{
NSLog(#"willCacheResponse");
// ...
}
Cocoa applies all sorts of criteria to determine whether it can cache. For example, in my experience, you will not see willCacheResponse called if the size of the response exceeds roughly 5% of the persistent storage cache size. I've also seen others claim that if max-age is smaller than 1835400, it won't cache either (this is not my experience, but perhaps older iOS versions suffered from this). Obviously, the request has to be a http or https request, not ftp or file request.
If my cache is large enough (and my response correctly supplies Cache-Control and Content-Type), I find that the cache works properly. I only wish that Apple would clearly articulate the criteria being applied (either in documentation or return codes) as I and others have wasted hours diagnosing cache failures only to realize that Cocoa was applying some secret rules.
Note, if NSURLConnection was, itself, satisfied by retrieving it from cache, willCacheResponse will not be called. And, of course ensure that didFailWithError was not called.
I've looked around a lot and cant seem to find a proper answer for my problem. As of now I have a network engine and I delegate into that from each of the view controllers to perform my network activity.
For example, to get user details I have a method like this:
- (void) getUserDetailsWithUserId:(NSString*) userId
{
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#Details", kServerAddress]]];
request.HTTPMethod = #"POST";
NSString *stringData = [NSString stringWithFormat:#"%#%#", kUserId, userId];
NSData *requestBodyData = [stringData dataUsingEncoding:NSUTF8StringEncoding];
request.HTTPBody = requestBodyData;
NSURLConnection *conn = [[NSURLConnection alloc] init];
[conn setTag:kGetUserInfoConnection];
(void)[conn initWithRequest:request delegate:self];
}
And when I get the data in connectionDidFinishLoading, I receive the data in a NSDictionary and based on the tag I've set for the connection, I transfer the data to the required NSDictionary.
This is working fine. But now I require two requests going from the same view controller. So when I do this, the data is getting mixed up. Say I have a connection for search being implemented, the data from the user details may come in when I do a search. The data is not being assigned to the right NSDictionary based on the switch I'm doing inside connectionDidFinishLoading. I'm using a single delegate for the entire network engine.
I'm new to NSURLConnection, should I setup a queue or something? Please help.
EDIT
Here's the part where I receive data in the connectionDidFinishLoading:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
if ([connection.tag integerValue] == kGetUserDetails)
networkDataSource.userData = self.jsonDetails;
if ([connection.tag integerValue] == kSearchConnection)
networkDataSource.searchData = self.jsonDetails;
}
and after this I have a switch case that calls the required delegate for the required view controller.
Anil here you need to identify for which request you got the data,
simplest way to check it is as below,
- (void)connectionDidFinishLoading:(NSURLConnection *)conn
{
// Check URL value for request, url for search and user details will be different, put if condition as per your need.
conn.currentRequest.URL
}
Try using conn.originalRequest.URL it will give original request.
You can do in many ways to accomplish your task as mentioned by others and it will solve your problem . But if you have many more connections , you need to change your approach.
You can cretae a subclass of NSOperation class. Provide all the required data, like url or any other any informmation you want to get back when task get accomplish , by passing a dictionary or data model to that class.
In Nsoperation class ovewrite 'main' method and start connection in that method ie put your all NSURRequest statements in that method. send a call back when download finish along with that info dict.
Points to be keep in mind: Create separte instance of thet operation class for evey download, and call its 'start method'.
It will look something like :
[self setDownloadOperationObj:[[DownloadFileOperation alloc] initWithData:metadataDict]];
[_downloadOperationObj setDelegate:self];
[_downloadOperationObj setSelectorForUpdateComplete:#selector(callBackForDownloadComplete)];
[_downloadOperationObj setQueuePriority:NSOperationQueuePriorityVeryHigh];
[_downloadOperationObj start];
metaDict will contain your user info.
In DownloadFileOperation class you will overwrite 'main' method like :
- (void)main {
// a lengthy operation
#autoreleasepool
{
if(self.isCancelled)
return;
// //You url connection code
}
}
You can add that operation to a NSOperationQueue if you want. You just need to add the operation to NSOperationQueue and it will call its start method.
Declare two NSURLConnection variables in the .h file.
NSURLConnection *conn1;
NSURLConnection *conn2;
- (void)connectionDidFinishLoading:(NSURLConnection *)connection{
if(connection == conn1){
}
else if(connection == conn2){
}
}