How do I repeat an ASIHTTPRequest? - iphone

Given the example code below:
// ExampleModel.h
#interface ExampleModel : NSObject <ASIHTTPRequestDelegate> {
}
#property (nonatomic, retain) ASIFormDataRequest *request;
#property (nonatomic, copy) NSString *iVar;
- (void)sendRequest;
// ExampleModel.m
#implementation ExampleModel
#synthesize request;
#synthesize iVar;
# pragma mark NSObject
- (void)dealloc {
[request clearDelegatesAndCancel];
[request release];
[iVar release];
[super dealloc];
}
- (id)init {
if ((self = [super init])) {
// These parts of the request are always the same.
NSURL *url = [[NSURL alloc] initWithString:#"https://example.com/"];
request = [[ASIFormDataRequest alloc] initWithURL:url];
[url release];
request.delegate = self;
[request setPostValue:#"value1" forKey:#"key1"];
[request setPostValue:#"value2" forKey:#"key2"];
}
return self;
}
# pragma mark ExampleModel
- (void)sendRequest {
// Reset iVar for each repeat request because it might've changed.
[request setPostValue:iVar forKey:#"iVarKey"];
[request startAsynchronous];
}
#end
# pragma mark ASIHTTPRequestDelegate
- (void)requestFinished:(ASIHTTPRequest *)request {
// Handle response.
}
- (void)requestFailed:(ASIHTTPRequest *)request {
// Handle error.
}
When I do something like [exampleModel sendRequest] from a UIViewController, it works! But, then I do [exampleModel sendRequest] again from another UIViewController and get:
Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '*** -[NSOperationQueue addOperation:]:
operation is finished and cannot be enqueued`
How can I fix this?

You shouldn't attempt to reuse the request object. It maintains state. Really designed to be disposed off after the request is over.
The design isn't as clean as the NSURLConnection, NSURLRequest, NSURLResponse classes (basically mashing all three into one and wrapping the low level core foundation classes underneath). It's still far better than using NSURLConnection in a vanilla fashion if you need to deal with low level HTTP stuff. If you don't, the high level classes have some advantages (like access to the same cache the UIWebView uses).

I think I found the answer: https://groups.google.com/d/msg/asihttprequest/E-QrhJApsrk/Yc4aYCM3tssJ

ASIHTTPRequest and its subclasses conform to the NSCopying protocol. Just do this:
ASIFormDataRequest *newRequest = [[request copy] autorelease];
[newRequest startAsynchronous];

Related

How can I cancel an asynchronous call through NSURLConnection sendAsynchronousRequest?

I've got a web service call performing some validation on user input in real time. I'd like to use [NSURLConnection sendAsynchronousRequest] on the validation (which was introduced in iOS 5), but cancel it if the user changes the input field content in the mean time. What is the best way to cancel a current request?
It doesn't appear that there is a good way to do this. The solution seems to be to not use the new [NSURLConnection sendAsynchronousRequest] in situations in which you need to cancel the request.
I've managed to do this by placing the sendAsynchronousRequest method in a separate DownloadWrapper class, as follows:
//
// DownloadWrapper.h
//
// Created by Ahmed Khalaf on 16/12/11.
// Copyright (c) 2011 arkuana. All rights reserved.
//
#import <Foundation/Foundation.h>
#protocol DownloadWrapperDelegate
- (void)receivedData:(NSData *)data;
- (void)emptyReply;
- (void)timedOut;
- (void)downloadError:(NSError *)error;
#end
#interface DownloadWrapper : NSObject {
id<DownloadWrapperDelegate> delegate;
}
#property(nonatomic, retain) id<DownloadWrapperDelegate> delegate;
- (void)downloadContentsOfURL:(NSString *)urlString;
#end
#implementation DownloadWrapper
#synthesize delegate;
- (void)downloadContentsOfURL:(NSString *)urlString
{
NSURL *url = [NSURL URLWithString:urlString];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:TIMEOUT_INTERVAL];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
if ([data length] > 0 && error == nil)
[delegate receivedData:data];
else if ([data length] == 0 && error == nil)
[delegate emptyReply];
else if (error != nil && error.code == ERROR_CODE_TIMEOUT)
[delegate timedOut];
else if (error != nil)
[delegate downloadError:error];
}];
}
#end
To utilise this class, I do the following, in addition to declaring the DownloadWrapper *downloadWrapper variable (in the interface declaration) and implementing the protocol methods which handles the response or a lack of one:
NSString *urlString = #"http://yoursite.com/page/to/download.html";
downloadWrapper = [DownloadWrapper alloc];
downloadWrapper.delegate = self;
[downloadWrapper downloadContentsOfURL:urlString];
Then I simply do the following to 'cancel' the connection when the view is about to disappear:
- (void)viewDidUnload
{
[super viewDidUnload];
downloadWrapper = nil;
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[downloadWrapper setDelegate:nil];
}
It's as simple as that. This would hopefully mimic the documented cancel method, which states that it does the following:
Once this method is called, the receiver’s delegate will no longer
receive any messages for this NSURLConnection.
I was concerned that this (somewhat naive) method means that the packets of data would still come through in response to our URL request - only that we're no longer 'listening in' as the delegate. But then I realised that once the URL request was sent through, there's really no way of stopping the response from coming back to us - we can only disregard it (if not at this level, then still at some lower level in the network hierarchy). Please correct me if I'm wrong.
Either way, hope this helps.

ASIHTTPRequest crashing when setting delegate?

It will be probably a simple problem, but I have been staring on this for a while now and I can't find it!
I have a SOAPRequest class like following:
#interface SoapRequest : NSObject
#property (retain, nonatomic) NSURL *endPoint;
#property (retain, nonatomic) NSString *soapAction;
#property (retain, nonatomic) NSString *userName;
#property (retain, nonatomic) NSString *passWord;
#property (retain, nonatomic) NSString *postData;
#property (retain, nonatomic) NSObject *handler;
#property SEL action;
+ (SoapRequest*) create: (NSObject*) target endPoint: (NSString*) endPoint action: (SEL) action soapAction: (NSString*) soapAction postData: (NSString*) postData;
- (id)sendSynchronous;
- (void) send;
#end
Implementation like following:
#synthesize endPoint = _endPoint, soapAction = _soapAction, userName = _userName, passWord = _passWord, postData = _postData, action = _action, handler = _handler;
+ (SoapRequest*) create: (NSObject*) target endPoint: (NSString*) endPoint action: (SEL) action soapAction: (NSString*) soapAction postData: (NSString*) postData
{
SoapRequest *request = [[SoapRequest alloc] init];
request.endPoint = [NSURL URLWithString:endPoint];
request.soapAction = soapAction;
request.handler = target;
request.action = action;
request.postData = postData;
return [request autorelease];
}
And my send function:
- (void) send
{
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:self.endPoint];
[request setDelegate:self];
[request addRequestHeader:#"Content-Length" value: [NSString stringWithFormat:#"%d",[self.postData length]]];
[request addRequestHeader:#"Content-Type" value:#"text/xml"];
if(self.soapAction)
{
[request addRequestHeader:#"soapAction" value:self.soapAction];
}
[request appendPostData:[self.postData dataUsingEncoding:NSUTF8StringEncoding]];
[request startAsynchronous];
}
I do have the default ASIHTTPRequest methods to listen for error or finish, my finish looks as following:
- (void)requestFinished:(ASIHTTPRequest *)request
{
[self.handler performSelector:self.action];
}
The case is, it crashes when I want to make this class a delegate for the ASIHTTPRequest, [request setDelegate:self]. I figured it has something to do with the Autoreleasing in the first function. But I don't have any idea how to fix it!
Edit:
This is how I init my SoapRequest:
- (SoapRequest*) DefinedEntities: (id) _target action: (SEL) _action
{
// some data inits
SoapRequest *request = [SoapRequest create: _target endPoint:endPoint action:_action soapAction:soapAction postData:xmlString];
[request send];
return request;
}
And this i init as following:
Metadata *service = [[Metadata alloc] init];
[service DefinedEntities:self action:#selector(MetadataFinished:)];
Had the same problem like you. The problem is, it gets deallocated before the delegate method gets called. Simply put a retain in your start method and a release in your finished method (and don't forget to release it in the error methods). This will let the object at life as long as the request is performing.
Please post the code where you're creating the instance of a SOAPRequest object using:
+ (SoapRequest*) create: (NSObject*) target endPoint: (NSString*) endPoint action: (SEL) action soapAction: (NSString*) soapAction postData: (NSString*) postData;
You may need to retain it. As an aside, you should use 'id' instead of NSObject* for your target parameter, as your target will not be an instance of NSObject (it will be a subclass) of sorts.
If the crash only occurs when you set self (SOAPRequest) as the delegate, are you sure that self isn't being deallocated (then the bad access occurs when the ASIHTTPRequest calls the did finish on its delegate which no longer exists).
- (SoapRequest*) DefinedEntities: (id) _target action: (SEL) _action
{
// some data inits
SoapRequest *request = [SoapRequest create: _target endPoint:endPoint action:_action soapAction:soapAction postData:xmlString];
[request send];
return request;
}
When you do this, request is autoreleased and you should make sure it is retained by someone until the ASIHTTPRequest it sent has finished or failed.
Retaining was indeed a good option, I found some typo's in my code and figured out what i did wrong already.
I had to do with the class in-between:
- (SoapRequest*) DefinedEntities: (id) _target action: (SEL) _action
{
// some data inits
SoapRequest *request = [SoapRequest create: _target endPoint:endPoint action:_action soapAction:soapAction postData:xmlString];
[request send];
return request;
}

Objective-C: Asynchronously populate UITableView - how to do this?

I can't seem to find any info on this question, so I thought I'd ask the community.
Basically, I have a UITableView and I want to show an activity indicator while its data is loading from my server.
Here is some example code of what I'm trying to do (I'm using ASIHttpRequest).
//self.listData = [[NSArray alloc] initWithObjects:#"Red", #"Green", #"Blue", #"Indigo", #"Violet", nil]; //this works
NSString *urlStr=[[NSString alloc] initWithFormat:#"http://www.google.com"]; //some slow request
NSURL *url=[NSURL URLWithString:urlStr];
__block ASIHTTPRequest *request=[ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[request setCompletionBlock:^{
self.listData = [[NSArray alloc] initWithObjects:#"Red", #"Green", #"Blue", #"Indigo", #"Violet", nil]; //this doesn't work...
[table reloadData];
}];
[request setFailedBlock:^{
}];
[request startAsynchronous];
The dummy request to google.com does nothing - it just creates a delay and in the response I hope to repopulate the table with some JSON response from my own website.
But when I try to populate the table with the colours, nothing happens! I just get a blank table... If I uncomment the line above, it works fine, it's just on http responses things don't work for me.
Any suggestions greatly appreciated.
Edit:
I did a [self.tableView reloadData]; and now it works...
Stop using ASIHTTPRequest. NSURLConnection is not hard to use and will result in better, more performant code.
Your JSON response should be fed into a data structure not the UI. I recommend Core Data.
The data structure should feed your UITableView. Again, I recommend Core Data.
I would suggest reviewing how MVC works, you are short circuiting the design and that is the core problem.
SPOILER
Here is a more detailed how to. First you want the data retrieval to be async. Easiest and most reusable way to do that is build a simple NSOperation subclass.
#class CIMGFSimpleDownloadOperation;
#protocol CIMGFSimpleDownloadDelegate <NSObject>
- (void)operation:(CIMGFSimpleDownloadOperation*)operation didCompleteWithData:(NSData*)data;
- (void)operation:(CIMGFSimpleDownloadOperation*)operation didFailWithError:(NSError*)error;
#end
#interface CIMGFSimpleDownloadOperation : NSOperation
#property (nonatomic, assign) NSInteger statusCode;
- (id)initWithURLRequest:(NSURLRequest*)request andDelegate:(id<CIMGFSimpleDownloadDelegate>)delegate;
#end
This subclass is the most basic way to download something from a URL. Construct it with a NSURLRequest and a delegate. It will call back on a success or failure. The implementation is only slightly longer.
#import "CIMGFSimpleDownloadOperation.h"
#interface CIMGFSimpleDownloadOperation()
#property (nonatomic, retain) NSURLRequest *request;
#property (nonatomic, retain) NSMutableData *data;
#property (nonatomic, assign) id<CIMGFSimpleDownloadDelegate> delegate;
#end
#implementation CIMGFSimpleDownloadOperation
- (id)initWithURLRequest:(NSURLRequest*)request andDelegate:(id<CIMGFSimpleDownloadDelegate>)delegate
{
if (!(self = [super init])) return nil;
[self setDelegate:delegate];
[self setRequest:request];
return self;
}
- (void)dealloc
{
[self setDelegate:nil];
[self setRequest:nil];
[self setData:nil];
[super dealloc];
}
- (void)main
{
[NSURLConnection connectionWithRequest:[self request] delegate:self];
CFRunLoopRun();
}
- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSHTTPURLResponse*)resp
{
[self setStatusCode:[resp statusCode]];
[self setData:[NSMutableData data]];
}
- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)newData
{
[[self data] appendData:newData];
}
- (void)connectionDidFinishLoading:(NSURLConnection*)connection
{
[[self delegate] operation:self didCompleteWithData:[self data]];
CFRunLoopStop(CFRunLoopGetCurrent());
}
- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error
{
[[self delegate] operation:self didFailWithError:error];
CFRunLoopStop(CFRunLoopGetCurrent());
}
#synthesize delegate;
#synthesize request;
#synthesize data;
#synthesize statusCode;
#end
Now this class is VERY reusable. There are other delegate methods for NSURLConnection that you can add depending on your needs. NSURLConnection can handle redirects, authentication, etc. I strongly suggest you look into its documentation.
From here you can either spin off the CIMGFSimpleDownloadOperation from your UITableViewController or from another part of your application. For this demonstration we will do it in the UITableViewController. Depending on your application needs you can kick off the data download wherever makes sense. For this example we will kick it off when the view appears.
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSURLRequest *request = ...;
CIMGFSimpleDownloadOperation *op = [[CIMGFSimpleDownloadOperation alloc] initWithURLRequest:request andDelegate:self];
[[NSOperationQueue mainQueue] addOperation:op];
[self setDownloadOperation:op]; //Hold onto a reference in case we want to cancel it
[op release], op = nil;
}
Now when the view appears an async call will go and download the content of the URL. In this code that will either pass or fail. The failure first:
- (void)operation:(CIMGFSimpleDownloadOperation*)operation didFailWithError:(NSError*)error;
{
[self setDownloadOperation:nil];
NSLog(#"Failure to download: %#\n%#", [error localizedDescription], [error userInfo]);
}
On success we need to parse the data that came back.
- (void)operation:(CIMGFSimpleDownloadOperation*)operation didCompleteWithData:(NSData*)data;
{
[self setDownloadOperation:nil];
NSLog(#"Download complete");
//1. Massage the data into whatever we want, Core Data, an array, whatever
//2. Update the UITableViewDataSource with the new data
//Note: We MIGHT be on a background thread here.
if ([NSThread isMainThread]) {
[[self tableView] reloadData];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[[self tableView] reloadData];
});
}
}
And done. A few more lines of code for you to write but it replaces 13K+ lines of code that gets imported with ASI resulting in a smaller, leaner, faster application. And more importantly it is an app that you understand every single line of code.
This is the problem
request setCompletionBlock:^{
self.listData = [[NSArray alloc] initWithObjects:#"Red", #"Green", #"Blue", #"Indigo", #"Violet", nil]; //this doesn't work...
[table performSelectorOnMainThread:#selector(reloadTable) withObject:nil waitUntilDone:NO];
}];
The reload table needs to be done on the main thread.
I tried NWCoder's solution, and it didn't work because its calling the wrong method. This is what i used.
[self.tableView performSelectorOnMainThread:#selector(reloadData) withObject:nil waitUntilDone:NO];

How to make NSURLConnection file download work?

I have a ViewController declared as:
#interface DownloadViewController : UIViewController
<UITableViewDataSource, UITableViewDelegate>
and I want to use NSURLConnection to download files. NSURLConnection simply "doesn't start", the delegate methods don't work (for example connection:didReceiveResponse is never called) . I noticed in some sample code that the class was subclassing NSObject instead of UIViewController.
How do I combine it? I want to use ViewController methods but then I can't use NSURLConnection.
It's not so easy to find a fully explained example how to download file with NSURLConnection. Everyone only concentrates on the easy methods like didReceiveResponse.
Using a UIViewController instead of an NSObject should not be your problem here !
I'm using a NSURLConnection in an UIViewController with no issue !
Here is a part of my code (not sure it will compile as it is) :
//
// MyViewController.h
//
#import <Foundation/Foundation.h>
#interface MyViewController : UIViewController {
#protected
NSMutableURLRequest* req;
NSMutableData* _responseData;
NSURLConnection* nzbConnection;
}
- (void)loadFileAtURL:(NSURL *)url;
#end
-
//
// MyViewController.m
//
#import "MyViewController.h"
#implementation MyViewController
- (void)loadView {
// create your view here
}
- (void) dealloc {
[_responseData release];
[super dealloc];
}
#pragma mark -
- (void)loadFileAtURL:(NSURL *)url {
// allocate data buffer
_responseData = [[NSMutableData alloc] init];
// create URLRequest
req = [[NSMutableURLRequest alloc] init];
[req setURL:_urlToHandle];
nzbConnection = [[NSURLConnection alloc] initWithRequest:req delegate:self startImmediately:YES];
[req release];
req = nil;
}
#pragma mark -
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// Append data in the reception buffer
if (connection == nzbConnection)
[_responseData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if (connection == nzbConnection) {
[nzbConnection release];
nzbConnection = nil;
// Print received data
NSLog(#"%#",_responseData);
[_responseData release];
}
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
// Something went wrong ...
if (connection == nzbConnection) {
[nzbConnection release];
[_responseData release];
}
}
#end
If you plan to download large files, consider storing the received packets in a file instead of storing it in memory !
If you're having problems, you could consider using the well regarded ASIHTTPRequest library to manage your download. It takes care of everything for you.
For example, just 2 lines will do it.
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDownloadDestinationPath:fullPathOfWhereToStoreFile];
Use "NSURLConnection asynchronously" search for the term and you'll find source. Or just NSURLConnection.
For example:
NSURLConnection NSURLRequest proxy for asynchronous web service calls
Using NSURLConnection from apple with example code
Objective-C Programming Tutorial – Creating A Twitter Client Part 1

Possible risk with asynchronous request and delegation

I would like to add to UIImageView the capacity to set an image with an url. As result I would like to do something like.
[anImageView setImageWithContentAtUrl:[NSURL URLWithString:#"http://server.com/resource.png"]];
So I created a category (code below).
NSString *kUserInfoImageViewKey = #"imageView";
NSString *kUserInfoActivityIndicatorKey = #"activityIndicator";
#implementation UIImageView (asynchronous)
#pragma mark -
- (void)setImageWithContentAtUrl:(NSURL *)imageUrl andActivityIndicator:(UIActivityIndicatorView *)activityIndicatorOrNil {
[activityIndicatorOrNil startAnimating];
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict setValue:self forKey:kUserInfoImageViewKey];
[dict setValue:activityIndicatorOrNil forKey:kUserInfoActivityIndicatorKey];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:imageUrl];
request.delegate = self;
request.userInfo = dict;
[dict release];
[request startAsynchronous];
}
#pragma mark -
#pragma mark private
- (void)requestFinished:(ASIHTTPRequest *)aRequest {
// get concerned view from user info
NSDictionary *dictionary = aRequest.userInfo;
UIImageView *imageView = (UIImageView *)[dictionary valueForKey:kUserInfoImageViewKey];
UIActivityIndicatorView *activityIndicator = (UIActivityIndicatorView *) [dictionary valueForKey:kUserInfoActivityIndicatorKey];
[activityIndicator stopAnimating];
NSData *responseData = [aRequest responseData];
UIImage * image = [[UIImage alloc] initWithData:responseData];
imageView.image = image;
[image release];
}
- (void)requestFailed:(ASIHTTPRequest *)request {
}
An ASIHTTPRequest is created and launched with the image as delegate. I think there is a risk if image is deallocated before ASIHTTPRequest return a result.
So, maybe adding a retain in setImageWithContentAtUrl: and adding a release in requestFinished: and requestFailed: but I'm not very confident.
How is it possible to do such things ?
Regards,
Quentin
Quentin,
I regularly use ASIHTTPRequest for asynchronous calls, so I know where you're coming from here. Also, it's a pain to set up for the first time, but did you know that Three20 library's TTImageView (I think that's it) already does what you are trying to do? It will even cache the image locally so you don't have to load it every time. Anyway.
Your worry is correct: ASIHTTPRequest is a wrapper on an NSOperation object (it's actually a subclass), so the NSOperationQueue will retain ASIHTTPRequest as long as the request is active.
If your user changes the view (say, on a nav bar controller), which then deallocs your UIImageView, your code may crash when it tries to call back to the delegate. So, when you dealloc your image view, it's better to hold on to a reference to the request and then cancel it.
Rather than a category, this may be one of those times where subclassing is better - because you'd want to overwrite the dealloc method (this is how I've handled this issue).
First, add this property to your subclass:
#property (nonatomic, retain) ASIHTTPRequest *request;
Then add this line to your method so you can hold on to it:
self.request = request;
And finally, in your ASIHTTPRequest delegate methods, destroy the reference:
self.request = nil;
Then your dealloc could look like this:
- (void) dealloc
{
if (self.request)
{
// Cancels the NSOperation so ASIHTTPRequest doesn't call back to this
[self.request cancel];
}
[request release];
[super dealloc]
}