How can I cancel an asynchronous call through NSURLConnection sendAsynchronousRequest? - iphone

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.

Related

How can I know when nsoperation queue has completed all requests so that I can reload my tableview?

I am downloading data from different links using ASIHTTPRequest and NSOperationQueue to
download in background thread. When a request has finished i parse in using requestFinished
delegate method of ASIHTTPRequest. I want to update the data in tableview when all requests in
the queue has completed. Is there any way to know when an NSOperationQueue has processed all
requests? i mean queue has any variable like 'isEmpty' or any delegate method like 'queueDidCompletedAllOperation'?
please help.
Here is the code:
//source
#interface SourceModel : NSObject
#property (nonatomic, retain) NSString * link;
#property (nonatomic, retain) NSString * name;
#end
//for rssGroup
#interface CompleteRSSDataModel : NSObject
#property (nonatomic,strong) SourceModel * source;
#property (nonatomic,strong) KissXMLParser * parser;
#property (nonatomic,strong) NSArray * rssArticles;
#end
- (void)viewDidLoad
{
for (int index=0; index<[rssGroups count]; index++) {
NSString * urlString = [[[rssGroups objectAtIndex:index] source] link];
NSURL *url = [NSURL URLWithString:urlString];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url]; [request setDelegate:self];
//set this request's tag to group index of this source(link). See requestFinished for use of this :)
[request setTag:index];
[self.queue addOperation:request];
}
}
- (void)requestFinished:(ASIHTTPRequest *)request {
NSLog(#"%#",#"RSS Data got from internet successfully :))");
int groupIndex = [request tag];
CompleteRSSDataModel * group = [rssGroups objectAtIndex:groupIndex];
group.parser = [[KissXMLParser alloc]initWithData:[request responseData]];
if (group.parser == nil) {
NSLog(#"%#",#"Failed - Error in parsing data :((");
}
else {
NSLog(#"%#",#"Data Parsed successfully :))");
group.rssArticles = [group.parser itemsInRss];
//So i want to check here that queue is empty, reload data, but as my information, i don't know any method like hasCompletedAllRequested
//if(self.queue hasCompletedAllRequests) {
// [self.tableview reloadData];
//}
}
}
- (void)requestFailed:(ASIHTTPRequest *)request {
NSLog(#"%#",#"Error in Getting RSS Data from internet:((");
}
If all operation has been completed then the operations array count will be zero.
To check this you can use Key Value Observation Coding to observer the operations key of NSOperationQueue
To set the observer for the key opertions will be like below:
[self.queue addObserver:self forKeyPath:#"operations" options:0 context:NULL];
Then do this in your observeValueForKeyPath like below:
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary *)change context:(void *)context
{
if (object == self.queue && [keyPath isEqualToString:#"operations"]) {
if ([self.queue.operations count] == 0) {
// Do something here when all operations has completed
NSLog(#"queue has completed");
}
}
else {
[super observeValueForKeyPath:keyPath ofObject:object
change:change context:context];
}
}
After iOS 4.0 you can use the property operationCount like self.queue.operationCount == 0 instead of checking like this [self.queue.operations count] == 0
I know this has already been answered but for future readers, if you are using AFNetworking and more specifically, AFHTTPRequestOperation you can do something like this:
NSString *urlPath = [NSString stringWithFormat:#"%#%#", baseUrl, file];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlPath]];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[operation setCompletionBlockWithSuccess:
^(AFHTTPRequestOperation *operation, id responseObject) {
if ([[queue operations] count] ==0) {
NSNotification * success = [NSNotification notificationWithName:#"TotalDone" object:[NSNumber numberWithBool: YES]];
[[NSNotificationCenter defaultCenter] postNotification:success];
queueSize = 0;
} else {
//get total queue size by the first success and add 1 back
if (queueSize ==0) {
queueSize = [[queue operations] count] +1.0;
}
float progress = (float)(queueSize-[[queue operations] count])/queueSize;
NSNumber * totProgress = [NSNumber numberWithFloat:progress];
NSNotification * totalProgressNotification = [NSNotification notificationWithName:#"TotalProgress"
object:totProgress];
[[NSNotificationCenter defaultCenter] postNotification:totalProgressNotification];
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error)
NSLog(#"Error: %#", error);
}];
This is for a downloader that adds a download to a NSOperationQueue, then notifies two progress bars: 1 for file progress, and 1 for total progress.
Hope this helps
A couple of options leap out at me:
Use ASINetworkQueue instead, and set queueDidFinishSelector, so it will tell you when it's done. This also gives you the opportunity to not only update UIProgressView views for not only the individual rows, but also another one for the entire download process.
Could you add something to your NSOperationQueue that would simply invoke a method to then update your UI (in the main queue, of course)? By adding it to the queue, it wouldn't get to it until it cleared the queue. Or in another queue, invoke waitUntilFinished, at which point you can update your UI.
Looking at your comments, it looks like you're updating your UI in requestFinished, but are dissatisfied because you think it might be better to wait for all of the updates to take place. Personally, I much prefer to update the UI, request by request, that way if it's slow, you get interim feedback, rather than waiting for everything. This has to be done gracefully, but I like updating the UI as I go along. Admittedly, some processes don't lend themselves to that.
On this last point, I think the trick is to do these updates so it's not distracting. Specifically, if your UI is a UITableView, you probably do not want to do a tableView.reloadData, which will reload the entire table. Instead, you probably want to check to see if the cell is still loaded (via cellForRowAtIndexPath, the UITableView method, not to be confused with the UITableViewController method tableView:cellForRowAtIndexPath) and if it is, update that one row, e.g.:
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
if (cell)
{
// you can either update the controls in the cell directly, or alternatively, just
// call reloadRowsAtIndexPaths to have the tableView use your data source:
[self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
}

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 do I repeat an ASIHTTPRequest?

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];

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

How to return an object from a class that uses NSURLConnection and it's delegate classes?

I'm in the process of trying to move code from a UITableViewController class to a "helper" class.
The code utilizes NSURLConnection to grab and parse JSON and then populate an NSMutableArray.
What I'd like to do is call a method in my helper class that returns a NSMutableArray. What I don't understand is how to return the array from the connectionDidFinishLoading delegate class of NSURLConnection (where the array is actually built) as though it was from the originally called method that started the connection. In other words, how does the method that calls NSURLConnection get control back so it can return a value from the whole operation?
Here are the relevant methods from the helper class. How do I get the getMovies method to return the listOfMovies that is built in the connectionDidFinishLoading delegate class?
-(NSMutableArray)getMovies:(NSURL*)url {
responseData = [[NSMutableData data] retain];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//NSURLRequest* request = [NSURLRequest requestWithURL: url cachePolicy: NSURLRequestUseProtocolCachePolicy timeoutInterval: 30.0];
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
[responseData setLength:0];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[responseData appendData:data];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
//TODO error handling for connection
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
//---initialize the array---
listOfMovies = [[NSMutableArray alloc] init];
tmdbMovies = [[NSArray alloc] init];
posters = [[NSArray alloc] init];
thumbs = [[NSDictionary alloc] init];
NSString *responseString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
SBJsonParser *json = [[SBJsonParser new] autorelease];
tmdbMovies = [json objectWithString:responseString];
// loop through all the top level elements in JSON
for (id movie in tmdbMovies) {
// 0 - Name
// 1 - Meta
// 2 - Url
if ((NSNull *)[movie objectForKey:#"name"] != [NSNull null]) {
if (![[movie objectForKey:#"name"] isEqualToString:#""]) {
name = [movie objectForKey:#"name"];
}
}
if ((NSNull *)[movie objectForKey:#"info"] != [NSNull null]) {
if (![[movie objectForKey:#"info"] isEqualToString:#""]) {
meta = [movie objectForKey:#"info"];
}
}
if ((NSNull *)[movie objectForKey:#"thumb"] != [NSNull null]) {
if (![[movie objectForKey:#"thumb"] isEqualToString:#""]) {
thumbUrl = [movie objectForKey:#"thumb"];
}
}
NSLog(#"Name: %#", name);
NSLog(#"Info: %#", meta);
NSLog(#"Thumb: %#", thumbUrl);
NSMutableArray *movieData = [[NSMutableArray alloc] initWithObjects:name,meta,thumbUrl,nil];
// add movieData array to listOfJMovies array
[listOfMovies addObject:movieData];
[movieData release];
}
//FIXME: Connection warning
if (connection!=nil) {
[connection release];
}
[responseData release];
[responseString release];
}
What you really need to do here is create a #protocol that creates a delegate for your helper class. Then change -(NSMutableArray)getMovies:(NSURL*)url to -(void)getMovies:(NSURL*)url
The class that is calling your helper method needs to implement your helper method's delegate.
Then - (void)connectionDidFinishLoading:(NSURLConnection *)connection calls the delegate method(s). It's best to have a one for success and one for failure.
=Update Begin=
You will need to also define an id delegate in your helper file which the calling class sets to self after init but before calling -(void)getMovies:(NSURL*)url. That way the helper file knows where to call back to.
getMovies *movieListCall = [[getMovies alloc] init];
movieListCall.delegate = self;
[movieListCall getMovies:<your NSURL goes here>];
You will see some additional lines for the inclusion of a delegate in both the getMovies.h and getMovies.m files.
=Update End=
in your getMovies.h file add:
#protocol getMoviesDelegate
#required
- (void)getMoviesSucceeded:(NSMutableArray *)movieArray;
- (void)getMoviesFailed:(NSString *)failedMessage;
#end
#interface getMovies : NSOBject {
id delegate;
}
#property (nonatomic, assign) id delegate;
in your getMovies.m file add:
#synthesize delegate;
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
//TODO error handling for connection
if ([delegate respondsToSelector:#selector(getMoviesFailed:)]) {
[delegate getMoviesFailed:[error localizedDescription]];
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
//finishes with
if ([delegate respondsToSelector:#selector(getMoviesSucceeded:)]) {
[delegate getMoviesSucceeded:listOfMovies];
}
}
update your calling class .h file to use getMoviesDelegate:
#interface MoviesView : UIViewController <getMoviesDelegate>{
.
.
.
}
add the getMoviesDelegate methods to your calling class .m file
- (void)getMoviesSucceeded:(NSMutableArray *)movieArray {
//deal with movieArray here
}
- (void)getMoviesFailed:(NSString *)failedMessage {
//deal with failure here
}
This is not tested but hopefully gives you a road map to work with.
Protocols are nice because you can make both required and optional delegate methods and it helps in refining your helper methods to become very reusable across projects. The compiler will also warn you if you have implemented a protocol but not implemented the protocol's required delegate methods. If you follow this path be sure to use conformsToProtocol: and respondsToSelector:
Fundamentally, what's happening is that you're starting an asynchronous network load (asynchronous is the right way to do this, almost assuredly), and then you need some way to resume whatever operation you were doing before the load began. You have a few options:
Create your own delegate protocol. Your UITableViewController would then set itself as the helper's delegate, and the helper would call helperDidLoad or whatever you named that method. There's more information on writing delegates in the Cocoa Programming Guide.
Use blocks and continuation passing style. This is a bit more advanced but I like it. In your UITableViewController you'd write something like this:
[helper doSomething:^ (id loaded) {
[modelObject refresh:loaded]; // or whatever you need to do
}];
And then in your helper you'd write:
- (void)doSomething:(void ^ (id))continuation {
_continuation = continuation;
//kick off network load
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
_continuation(_data);
}
Use notifications. Read the NSNotificationCenter docs.
Use KVO. The KVO programming guide has a lot of good info on Key-Value Observing.
How to i get the getMovies method to return the listOfMovies that is built in the connectionDidFinishLoading delegate class?
I'm going to argue that you should not do that.
Network requests should be made asynchronously. If your getMovies were to make a synchronous request and return only when it had data you would block that entire thread while you waiting for a network connection to finish. This is a bad idea in general and a terrible idea if your main thread is calling getMovies. Blocking the main thread will prevent you from responding to touches or updating the UI, your app will appear frozen, and the OS will terminate it if your users don't quit in frustration first.
Instead have the helper class notify the caller when data is available (or when it failed to retrieve data) through a delegate call back, notification, KVO, or whatever mechanism you prefer.
Here are the steps, pseudocode like style:
[helperInstance setDelegate:self]; // where self is your UITableViewController class
in your helper class, in the connectionDidFinishLoading do something like this:
[delegate finishedLoadingData:JSONData];
Also you can define a protocol for your delegate, and the declare the delegate like this in your helper class:
#property (nonatomic, assign) id<YourProtocol> delegate;
Hope this helps,
Moszi