In order to send HTTP request, I am using NSURLConnection like this:
NSURLConnection *connection = [[NSURLConnection alloc]
initWithRequest:request
delegate:self
startImmediately:YES];
At the end of connectionDidFinishLoading, I need to post different notifications, depending on the HTTP request that was just completed.
However inside connectionDidFinishLoading I don't have a clear logical identifier to the type of the request that was send:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// here i want to post various notifications, depending on the HTTP request that was completed
}
What is the best solution here? Thanks!
Connection did finish method passes the NSURLConnection object, which has the request and url:
-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(#"currentRequest: %#", connection.currentRequest);
NSLog(#"originalRequest: %#", connection.originalRequest);
// here do a if statement that compares url
if ([connection.currentRequest.URL.absoluteString isEqualToString:#"http://google.co.uk"]) {
NSLog(#"Equal to google");
// post notification
}
}
You can use framework like MKNetworkKit. In this framework you can write like this:
- (void) sendRequest: (NSString*) aRequestPath
httpMethod: (NSString*) aHttpMethod
paramsBlock: (SMFillParametersForRequestBlock) aParamsBlock
successBlock: (SMSaveRequestResultBlock) aSuccessBlock
errorBlock: (SMErrorRequestResultBlock) aErrorBlock
userInfo: (id) anUserInfo
{
MKNetworkEngine* network_engine= [[MKNetworkEngine alloc] initWithHostName: MuseumsHostName];
NSMutableDictionary* params = [[NSMutableDictionary alloc] init];
if (aParamsBlock)
{
aParamsBlock(params);
}
MKNetworkOperation* operation = [network_engine operationWithPath: aRequestPath
params: params
httpMethod: aHttpMethod
ssl: NO];
[operation onCompletion: ^(MKNetworkOperation* completedOperation){
// parse response of current request:
aSuccessBlock(completedOperation, anUserInfo, ...);
} onError: ^(NSError *error){
// error handler: call block
}];
[network_engine enqueueOperation: operation];
}
Believe me, this is the best solution
Related
I've read through tons of messages saying the same thing all over again : when you use a NSURLConnection, delegate methods are not called. I understand that Apple's doc are incomplete and reference deprecated methods, which is a shame, but I can't seem to find a solution.
Code for the request is there :
// Create request
NSURL *urlObj = [NSURL URLWithString:url];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:urlObj cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:30];
[request setValue:#"gzip" forHTTPHeaderField:#"Accept-Encoding"];
if (![NSURLConnection canHandleRequest:request]) {
NSLog(#"Can't handle request...");
return;
}
// Start connection
dispatch_async(dispatch_get_main_queue(), ^{
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]; // Edited
});
...and code for the delegate methods is here :
- (void) connection:(NSURLConnection *)_connection didReceiveResponse:(NSURLResponse *)response {
NSLog(#"Receiving response: %#, status %d", [(NSHTTPURLResponse*)response allHeaderFields], [(NSHTTPURLResponse*) response statusCode]);
self.data = [NSMutableData data];
}
- (void) connection:(NSURLConnection *)_connection didFailWithError:(NSError *)error {
NSLog(#"Connection failed: %#", error);
[self _finish];
}
- (void) connection:(NSURLConnection *)_connection didReceiveData:(NSData *)_data {
[data appendData:_data];
}
- (void)connectionDidFinishDownloading:(NSURLConnection *)_connection destinationURL:(NSURL *) destinationURL {
NSLog(#"Connection done!");
[self _finish];
}
There's not a lot of error checking here, but I've made sure of a few things :
Whatever happens, didReceiveData is never called, so I don't get any data
...but the data is transfered (I checked using tcpdump)
...and the other methods are called successfully.
If I use the NSURLConnectionDownloadDelegate instead of NSURLConnectionDataDelegate, everything works but I can't get a hold on the downloaded file (this is a known bug)
The request is not deallocated before completion by bad memory management
Nothing changes if I use a standard HTML page somewhere on the internet as my URL
The request is kicked off from the main queue
I don't want to use a third-party library, as, ultimately, these requests are to be included in a library of my own, and I'd like to minimize the dependencies. If I have to, I'll use CFNetwork directly, but it will be a huge pain in the you-know-what.
If you have any idea, it would help greatly. Thanks!
I ran into the same problem. Very annoying, but it seems that if you implement this method:
- (void)connectionDidFinishDownloading:(NSURLConnection *)connection destinationURL:(NSURL *)destinationURL
Then connection:didReceiveData: will never be called. You have to use connectionDidFinishLoading: instead... Yes, the docs say it is deprecated, but I think thats only because this method moved from NSURLConnectionDelegate into NSURLConnectionDataDelegate.
I like to use the sendAsynchronousRequest method.. there's less information during the connection, but the code is a lot cleaner.
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error){
if (data){
//do something with data
}
else if (error)
NSLog(#"%#",error);
}];
From Apple:
By default, a connection is scheduled on the current thread in the
default mode when it is created. If you create a connection with the
initWithRequest:delegate:startImmediately: method and provide NO for
the startImmediately parameter, you can schedule the connection on a
different run loop or mode before starting it with the start method.
You can schedule a connection on multiple run loops and modes, or on
the same run loop in multiple modes.
Unless there is a reason to explicitly run it in [NSRunLoop currentRunLoop],
you can remove these two lines:
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];
or change the mode to NSDefaultRunLoopMode
NSURLConnection API says " ..delegate methods are called on the thread that started the asynchronous load operation for the associated NSURLConnection object."
Because dispatch_async will start new thread, and NSURLConnection will not pass to that other threat the call backs, so do not use dispatch_async with NSURLConnection.
You do not have to afraid about frozen user interface, NSURLConnection providing only the controls of asynchronous loads.
If you have more files to download, you can start some of connection in first turn, and later they finished, in the connectionDidFinishLoading: method you can start new connections.
int i=0;
for (RetrieveOneDocument *doc in self.documents) {
if (i<5) {
[[NSURLConnection alloc] initWithRequest:request delegate:self];
i++;
}
}
..
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
ii++;
if(ii == 5) {
[[NSURLConnection alloc] initWithRequest:request delegate:self];
ii=0;
}
}
One possible reason is that the outgoing NSURLRequest has been setup to have a -HTTPMethod of HEAD. Quite hard to do that by accident though!
I am using the IOS sdk to upload file, works fine over Wifi, but over 3G sometimes on large files the following error is caught in
- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)anError .....
"NSURLErrorDomain -1021 request body stream exhausted".
I know i can override this problem by implementing the method:
- (NSInputStream*)connection:(NSURLConnection *)connection needNewBodyStream:(NSURLRequest *) request
so i did it. But when this method called i caught in didFailWithError next error "The operation couldn’t be completed. Cannot allocate memory".
This error disappears if I add to method needNewBodyStream some delay.
Can somebody explain me for what need this delay, and how can i get rid of this hack?
here is my code:
- (void) startUpload
{
NSInputStream* fileStream = [[NSInputStream alloc] initWithFileAtPath: sourcePath];
[self.request setHTTPMethod:#"PUT"];
[self.request setValue:[NSString stringWithFormat: #"%lu", fileSize] forHTTPHeaderField: #"Content-Length"];
[self.request setHTTPBodyStream: fileStream];
NSURLConnection* newConnection = [[NSURLConnection alloc] initWithRequest: self.request delegate: self startImmediately: YES];
self.connection = newConnection;
[newConnection release];
[fileStream release];
}
#pragma mark NSURLConnectionDelegate
- (NSInputStream *) connection: (NSURLConnection *) aConnection needNewBodyStream: (NSURLRequest *) request
{
[NSThread sleepForTimeInterval: 2];
NSInputStream* fileStream = [NSInputStream inputStreamWithFileAtPath: sourcePath];
if (fileStream == nil)
{
NSLog(#"NSURLConnection was asked to retransmit a new body stream for a request. Returning nil will cancel the connection.");
}
return fileStream;
}
Short version of the question:
What is wrong with the following Kiwi/iOS mock expectation?
[[mockDelegate should] receive:#selector(connectionDidSucceedWithText:andStatus:) withArguments:[testString1 stringByAppendingString:testString2],theValue(value),nil];
Long version of question:
I am trying to write a test in Kiwi, iOS for a simple class that handles a NSConnection. To test that the class handles the callback from the NSConnection I send it the delegate methods NSConnection normally does. I have a delegate in the class that sends data back to whoever uses my class. To test my class I have to inject a mocked delegate and then check that my desired methods are called. Simple as that :)
My code for the Kiwi test is:
//Some ivars declared elsewhere:
testString1 = #"asd323/4 d14";
testString2 = #"as98 /2y9h3fdd14";
testData1 = [testString1 dataUsingEncoding:NSUTF8StringEncoding];
testData2 = [testString2 dataUsingEncoding:NSUTF8StringEncoding];
mockURLRespons = [NSHTTPURLResponse mock];
int value = 11111;
id mockDelegate = [KWMock mockForProtocol:#protocol(SharepointConnectionDelegate)];
communicator = [[SharepointCommunicator alloc] init];
it (#"should send recieve data back to delegate2", ^{
[communicator setDelegate:mockDelegate];
[mockURLRespons stub:#selector(statusCode) andReturn:theValue(value)];
[(id)communicator connection:niceMockConnector didReceiveResponse:mockURLRespons];
[(id)communicator connection:niceMockConnector didReceiveData:testData1];
[(id)communicator connection:niceMockConnector didReceiveData:testData2];
[(id)communicator connectionDidFinishLoading:niceMockConnector];
[[mockDelegate should] receive:#selector(connectionDidSucceedWithText:andStatus:) withArguments:[testString1 stringByAppendingString:testString2],theValue(value),nil];
});
And in my SharepointCommunicator.m:
-(void)connection:(NSURLConnection *)aConnection didReceiveResponse:(NSURLResponse *)response {
if (connection != aConnection) {
[connection cancel];
connection = aConnection;
}
responseData = [[NSMutableData alloc] init];
statusCode = [(NSHTTPURLResponse*)response statusCode];
}
-(void)connection:(NSURLConnection *)aConnection didReceiveData:(NSData *)data {
if (aConnection != self.connection)
return;
[responseData appendData:data];
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
NSString *txt = [[NSString alloc] initWithData:responseData encoding: NSASCIIStringEncoding];
NSLog(#"Statuscode: %i", statusCode);
NSLog(#"Data is: %#",txt);
[delegate connectionDidSucceedWithText:txt andStatus:statusCode];
[self.connection cancel];
self.connection = nil;
}
This code works and is correct. Debugging it with checkpoint shows it does as expected. The values of statusCode is 11111. and txt is testString1+textString2. Still it fails on the last row on in the test with the following error:
error: -[kiwiSharepointCommunicatorTest Sharepointcommunicator_AStateTheComponentIsIn_ShouldSendRecieveDataBackToDelegate2] : 'Sharepointcommunicator, a state the component is in, should send recieve data back to delegate2' [FAILED], mock received unexpected message -connectionDidSucceedWithText:"asd323/4 d14as98 /2y9h3fdd14" andStatus:11111
Test Case '-[kiwiSharepointCommunicatorTest Sharepointcommunicator_AStateTheComponentIsIn_ShouldSendRecieveDataBackToDelegate2]' failed (3.684 seconds).
Removing the last row in the test still generate the same error. I guess my understanding of receive:withArguments: is wrong..
You have to call [[mockDelegate should] receive... before the call to connectionDidFinishLoading to prepare the mockDelegate for the message it's about to receive.
In my first ViewController (MonitorViewController) this is in the interface file MonitorViewController.h:
#import <RestKit/RestKit.h>
#interface MonitorViewController : UIViewController <RKRequestDelegate>
In MonitorViewController.m ViewDidLoad method, I have this at the end:
RKClient* client = [RKClient clientWithBaseURL:#"http://192.168.2.3:8000/DataRecorder/ExternalControl"];
NSLog(#"I am your RKClient singleton : %#", [RKClient sharedClient]);
[client get:#"/json/get_Signals" delegate:self];
The implementation of delegate methods in MonitorViewController.m:
- (void) request: (RKRequest *) request didLoadResponse: (RKResponse *) response {
if ([request isGET]) {
NSLog (#"Retrieved : %#", [response bodyAsString]);
}
}
- (void) request:(RKRequest *)request didFailLoadWithError:(NSError *)error
{
NSLog (#"Retrieved an error");
}
- (void) requestDidTimeout:(RKRequest *)request
{
NSLog(#"Did receive timeout");
}
- (void) request:(RKRequest *)request didReceivedData:(NSInteger)bytesReceived totalBytesReceived:(NSInteger)totalBytesReceived totalBytesExectedToReceive:(NSInteger)totalBytesExpectedToReceive
{
NSLog(#"Did receive data");
}
My AppDelegate method DidFinishLaunchingWithOptions method only returns YES and nothing else.
I recommend using RestKit framework. With restkit, you simply do:
// create the parameters dictionary for the params that you want to send with the request
NSDictionary* paramsDictionary = [NSDictionary dictionaryWithObjectsAndKeys: #"00003",#"SignalId", nil];
// send your request
RKRequest* req = [client post:#"your/resource/path" params:paramsDictionary delegate:self];
// set the userData property, it can be any object
[req setUserData:#"SignalId = 00003"];
And then, in the delegate method:
- (void)request:(RKRequest *)request didLoadResponse:(RKResponse *)response {
// check which request is responsible for the response
// to achieve this, you can do two things
// check the parameters of the request like this
NSLog(#"%#", [request URL]); // this will print your request url with the parameters
// something like http://myamazingrestservice.org/resource/path?SignalId=00003
// the second option will work if your request is not a GET request
NSLog(#"%#", request.params); // this will print paramsDictionary
// or you can get it from userData if you decide to go this way
NSString* myData = [request userData];
NSLog(#"%#", myData); // this will log "SignalId = 00003" in the debugger console
}
So you will never need to send the parameters that are not used on the server side, just to distinguish your requests. Additionally, the RKRequest class has lots of other properties that you can use to check which request corresponds to the given response. But if you send a bunch of identical requests, I think the userData is the best solution.
RestKit will also help you with other common rest interface tasks.
I am using facebook latest iphone sdk, I am making multiple request to facebook from different places in my app. For all of them didReceiveResponse and didLoad methods get called, it is very difficult to find out from didLoad method that for what request this response was so I am wondering if didReceiveResponse can help, can i retrieve some information in this method which will tell me what was the request for which I have got the response.
The way I do it is pretty much the same way Ziminji does it, but in didLoad method:
- (void)request:(FBRequest *)request didLoad:(id)result
{
NSLog(#"Facebook request %# loaded", [request url]);
//handling a user info request, for example
if ([[request url] rangeOfString:#"/me"].location != NSNotFound)
{
/* handle user request in here */
}
}
So basically you need only to check the url you sent the request to, and you can also check the parameters for that request. Then you can differentiate one from another.
All I am doing here is I am checking for some unique attribute in the response and relating it to the request, I know this is not the best way to do is, but this is what I have found so far, please let me know if any one is doing it any different
You could try something like the following:
- (void) request: (FBRequest *)request didReceiveResponse: (NSURLResponse *)response {
NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
if (statusCode == 200) {
NSString *url = [[response URL] absoluteString];
if ([url rangeOfString: #"me/feed"].location != NSNotFound) {
NSLog(#"Request Params: %#", [request params]);
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: #"Facebook" message: #"Message successfully posted on Facebook." delegate: nil cancelButtonTitle: #"OK" otherButtonTitles: nil];
[alert show];
[alert release];
}
}
}
If you retain the request object as a property of your request delegate when you create it, you can check to see if it matches on the delegate method calls. For example:
- (void)queryForUserInfo {
self.userInfoRequest = [facebook requestWithGraphPath:#"me" andDelegate:self];
}
#pragma mark <FBRequestDelegate>
- (void)request:(FBRequest *)request didLoad:(id)result {
if (request == self.userInfoRequest) {
[self handleUserInfoResult:result];
}
}