Hey! I need to know how I can have my iOS Application start a download in the background of the application (like, have the download run in the AppDelegate file) so changing ViewControllers will not interrupt or cancel the download. I also need to be able to get the progress of the download (0.00000 - 1.00000), to set a UIProgressView object to, which also means I need a - (void)progressDidChangeTo:(int)progress function.
Just use ASIHTTPRequest it is way easier than NSURLRequest and does exactly what you need.
It examples that shows how to download in background and how to report progress.
I wouldn't download anything in the AppDelegate directly. Instead I would create a separated class just for that purpose. Let's call it MyService I would then initialize that class in my app delegate.
The class can work as a singleton or can be passed to each view controller that requires it.
In MyService class I would add the ASINetworkQueue and few methods to handle the requests when they are ready. Here is the code from ASI examples that you can use:
- (IBAction)startBackgroundDownloading:(id)sender
{
if (!self.queue) {
self.queue = [[[ASINetworkQueue alloc] init] autorelease];
}
NSURL *url = [NSURL URLWithString:#"http://allseeing-i.com"];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[request setDidFinishSelector:#selector(requestDone:)];
[request setDidFailSelector:#selector(requestWentWrong:)];
[self.queue addOperation:request]; //queue is an NSOperationQueue
[self.queue go];
}
- (void)requestDone:(ASIHTTPRequest *)request
{
NSString *response = [request responseString];
//Do something useful with the content of that request.
}
- (void)requestWentWrong:(ASIHTTPRequest *)request
{
NSError *error = [request error];
}
If you need to set the progress bar. I would just expose the setDownloadProgressDelegate of ASINetworkQueue in my MyService class and set it in my ViewControllers like that:
[[MyService service] setDownloadProgressDelegate: self.myUIProgressView];
BTW. If you need to continue downloading even when your app exits you can set ShouldContinueWhenAppEntersBackground property of your request to YES.
you can use NSURLConnection to start an asynchronous request that won't cause your UI to be frozen. You can do it by doing something like:
NSURLRequest *urlRequest = [[NSURLRequest alloc] initWithURL:url];
connection = [[NSURLConnection alloc] initWithRequest:urlRequest delegate:self];
[urlRequest release];
in order to have your progress you can use the:
connection:didReceiveResponse:(NSURLResponse *)response;
delegate call to inspect the response.expectedContentLength and then use the
connection:didReceiveData:(NSData *)data
to track the amount of data that was downloaded and calculate a percentage.
Hope this helps,
Moszi
Related
I'm making a server based app. Each time I do a request to the database, I have to type all the server connection code. Is it possible to reuse this somehow? In php, you usually have a file call dbConnect.php (or something similar) that you can call each time you want to connect.
Example, I would like to replace this, which I use all the time:
- (void)doSomething
{
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL: url];
__weak ASIHTTPRequest *request_b = request;
[request setDelegate: self];
[request addRequestHeader:#"Content-Type" value:#"text/html; charset=utf-8;"];
[request setDefaultResponseEncoding:NSUTF8StringEncoding];
[request setTimeOutSeconds: 10.0f];
[request setCachePolicy: ASIDoNotWriteToCacheCachePolicy | ASIDoNotReadFromCacheCachePolicy];
//Set the variables here
[request startAsynchronous];
}
... with something like:
- (void)doSomething
{
LoadServerCode; //This loads all the server code as above
//Set variables
[request startAsynchronous];
}
Thanks in advance
EDIT:
To clarify a little bit. Say I have some methods I use a lot, like creating a UILabel, or a UIView in a special way... It would be nice not to have to subclass, and end up with a bunch of classes, but rather have one class called MyConstructionMethods or something... So if I want to create a label on some different places in the app, I can just type:
MyGreenLabel; //Done, the label is created and added to the view
... instead of:
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 320, 10)];
label.backgroundColor = [UIColor greenColor];
[self.view addSubview: label];
Hopefully you are keeping all your connection classes isolated from the rest of your code, then why cant you just make a method that will create your request set your vars and return the request for you to start async...Even if you are not keep your connection stuff isolated you can still have a static method of some class have this method...
you could implement your custom ASIHTTPRequest class:
#interface YourRequest : ASIFormDataRequest
#end
#implementation YourRequest
- (id)initWithURL:(NSURL *)newURL {
self = [super initWithURL: newURL];
if (self) {
[self addRequestHeader:#"Content-Type" value:#"text/html; charset=utf-8;"];
[self setDefaultResponseEncoding:NSUTF8StringEncoding];
[self setTimeOutSeconds: 10.0f];
[self setCachePolicy: ASIDoNotWriteToCacheCachePolicy | ASIDoNotReadFromCacheCachePolicy];
}
}
return self;
}
#end
and create the object:
- (void)doSomething {
__block YourRequest *request = [YourRequest requestWithURL: url];
__weak ASIHTTPRequest *request_b = request;
[request setDelegate : self];
//Set variables
[request startAsynchronous];
}
Have you tried using a macro?
In your .h file:
#define LoadServerCode() \
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url]; \
__weak ASIHTTPRequest *request_b = request; \
[request setDelegate: self]; \
[request addRequestHeader:#"Content-Type" value:#"text/html; charset=utf-8;"]; \
[request setDefaultResponseEncoding:NSUTF8StringEncoding]; \
[request setTimeOutSeconds: 10.0f]; \
[request setCachePolicy: ASIDoNotWriteToCacheCachePolicy | ASIDoNotReadFromCacheCachePolicy];
Then, in your implementation:
-(void)doSomething {
LoadServerCode();
//Set variables
[request startAsynchronous];
}
If you need to reuse that setup code multiple times in the same implementation file, consider refactoring with Extract Method to create a utility method that returns a properly-configured request object.
If you need to do this kind of thing in numerous places, consider subclassing ASIFormDataRequest so you can more succinctly create request objects with the properties configured as you most commonly set them. Alternatively, you could create some kind of request factory class with static methods for generating request objects.
you can declare methods in your header file and those will be available when using your class. so you could declare doSomthing in your .h file then implement that method in your .m file and when you want to "doSomthing" just call [className doSomthing]
if you want to show more code i can probably give you a better example
What is the best way to check which request is which inside the delegate method:
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
}
Right now I have a NSURLConnection that I set to the NSURLConnection before making a request and inside didReceiveResponse I do:
if (self.tempConnection == connection)
however there is a possiblity this won't work for race conditions. Is there a better way to do this?
There is a better way in OS5. Forget about all those bothersome delegate messages. Let the connection build the data for you, and put your finished code right in line with your start code:
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"http://www.site.com"]];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
NSLog(#"got response %d, data = %#, error = %#", [httpResponse statusCode], data, error);
}];
I've looked at a bunch of different ways to do this, and I've found that by far the cleanest and easiest in order to manage is to use a block pattern. That way you are guaranteed to be responding to the right request upon completion, avoid race conditions, and you don't have any issues with variables or objects going out of scope during the asynchronous call. It's also a lot easier to read/maintain your code.
Both ASIHTTPRequest and AFNetworking APIs provide a block pattern (however ASI is no longer supported so best to go with AFNetworking for new stuff). If you don't want to use one of these libraries, but want to do it yourself, you can download the source for AFNetworking and review their implementation. However, that seems like a lot of extra work for little value.
Consider creating a separate class to serve as the delegate. Then, for each NSURLConnection spawned, instantiate a new instance of the delegate class to for that NSURLConnection
Here's some brief code to illustrate this:
#interface ConnectionDelegate : NSObject <NSURLConnectionDelegate>
...then implement the methods in the .m file
Now, I'm guessing you probably have the code you posted in a UIViewController subclass (or some other class serving different purposes)?
Wherever you are kicking off the requests, use this code:
ConnectionDelegate *newDelegate = [[ConnectionDelegate alloc] init];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"<url here">]];
[NSURLConnection connectionWithRequest:request delegate:newDelegate];
//then you can repeat this for every new request you need to make
//and a different delegate will handle this
newDelegate = [[ConnectionDelegate alloc] init];
request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"<url here">]];
[NSURLConnection connectionWithRequest:request delegate:newDelegate];
// ...continue as many times as you'd like
newDelegate = [[ConnectionDelegate alloc] init];
request = [NSURLRequest requestWithURL:[NSURL URLWithString:#"<url here">]];
[NSURLConnection connectionWithRequest:request delegate:newDelegate];
You might consider storing all the delegate objects in a NSDictionary or some other data structure to keep track of them. I'd consider using an NSNotification in connectionDidFinishLoading to post a notification that the connection is done, and to serve whatever object created from the response. Lemme know if you want code to help you visualize that. Hope this helps!
if i use ASIHTTPRequest the first time , with asynchroun mode , i recive result and error in
(void)requestFinished:(ASIHTTPRequest *)request
and the error in
(void)requestFailed:(ASIHTTPRequest *)request
but what if I make another query asynchronously? I will receive the result in the same method? how to know if this is the result of the first query or second? I tryed to change the delegate but it don't work
-(void)getCities
{
NSString * myURLString = [NSString stringWithFormat:#"http://localhost:8080/Data/resources/converter.city/CountryCode/%#",choosedCodeCity];
NSURL *url =[NSURL URLWithString:myURLString];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:resultCities];
[request startAsynchronous];
}
-(void)resultCities :(ASIHTTPRequest *)request
{
}
you can do:
request.didFinishSelector = #selector(yourFinishMethodHere:);
request.didFailSelector = #selector(yourFailMethodHere:);
in other words, you do not have to use the default "requestFinished:" and "requestFailed:" methods on the delegate.
ASIHttpRequest has a NSDictionary* userInfo property that you can use to store whatever you like. You can simply add a flag to this dictionary in order to tell the two requests apart.
Another approach would be to use the ASIHttpRequest methods that take blocks instead of using a delegate.
EDIT:
To use the flag approach, when you create the request object, do something like
[request.userInfo setObject:[NSNumber numberWithInt:1] forKey:#"flag"];
Then in the response methods you can ask the request for the flag to determine which request it is.
int flag = [[request.userInfo objectForKey:#"flag"] intValue];
Kinda stuck on this problem and I'm not sure, where I've gone wrong. Heres what I'm doing:
Class calls:
- (void)updateApplicationDataInBackground {
updateView = [[UpdatingView alloc] init];
[self.view addSubview:updateView.view];
DataSynchronizer *dataSynchronizer = [[DataSynchronizer alloc] init];
[NSThread detachNewThreadSelector:#selector(initWithDataRequest:) toTarget:dataSynchronizer withObject:self];
[dataSynchronizer release];
This creates a thread to retrieve data from the server and parse it. In DataSynchronizer this is the method being called:
- (void)initWithDataRequest:(id)parent {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
controller = parent;
NSLog(#"DataSynchronizer initWithDataRequest called");
NSURL *url = [NSURL URLWithString: ApiUrl];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setPostValue:ApiKey forKey:#"key"];
[request setPostValue:ApiPass forKey:#"password"];
[request setPostValue:#"somevalue" forKey:#"framework"];
[request setPostValue:#"somevalue" forKey:#"method"];
[request setDidFinishSelector:#selector(parseResult:)];
[request setDidFailSelector:#selector(requestError:)];
[request setTimeOutSeconds:60];
[request setDelegate:self];
[request startAsynchronous];
[pool release];
After my data is received I parse the contents and do my data synch. This is all working as expected. I've decided to throw in a UIProgressView so the user can see what is going on with this request, this progress view lives in updateView which is created in the updateApplicationDataInBackground.
I'm not trying to show progress for the web service call but simply when milestones are reached in the data processing. In the DidFinishSelector its calling parseResult
There are five method its calls with the response data:
[self parseData:[data objectForKey:#"types"] forObject:[Types class] andParent:nil];
[controller performSelectorOnMainThread:#selector(updateProgress:) withObject:[NSNumber numberWithFloat:.4] waitUntilDone:YES];
After each process I'm trying to update the UIProgressView, it will never update. Now if I simply call performSelectorOnMainThread from outside the ASIHTTPRequest it works as expected, but not within the DidFinishSelector. I've tried many variations on this where it calls a local method which updates the mainThread, where I simply use performSelector. Nothing works, how do I update the the UIProgessView?
Is the problem a thread spawning a thread?
Thanks
EDIT:
Looks like the DidFinishSelector is being called on the main thread already. I've updated my code to simply call:
[controller updateProgress:[NSNumber numberWithFloat:.8]]
Still no luck....
Realized it might be helpful to see the UIProgessView update method.
- (void)updateProgress:(NSNumber *)progress {
float newProgess = [progress floatValue];
[updateView.myProgress setProgress: newProgess];
Ok so it looks like I found my own answer after changing somethings around. Because ASIHttpRequest performs SetDidFinish selector on the main thread my calls performSelectorOnMainThread weren't doing anything. I changed my initial call for the DataSynchronizer to the main thread and added changed the DidFinish method to:
- (void)parseDataInBackground:(ASIHTTPRequest *)request {
[NSThread detachNewThreadSelector:#selector(parseResult:) toTarget:self withObject:request];
Which then makes the parse method run on separate thread (since its the bulk of the processing and now performOnMainThread works without issue.
I'm successfully making a ASIFormDataRequest using the below code.
//get groups
if (![self queue]) {
[self setQueue:[[[NSOperationQueue alloc] init] autorelease]];
}
//make the url by appending the URL from the Constant class to the jsp name
NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:#"%#/%#", URL, #"connectors/searchGroupsServlet.jsp"]];
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request addRequestHeader:#"User-Agent" value:USER_AGENT];
[request addPostValue:[login username] forKey:#"username"];
[request addPostValue:[login password] forKey:#"password"];
[request addPostValue:[searchText lowercaseString] forKey:#"query"];
[request addPostValue:GROUP_FILTER_LIMIT forKey:#"limit"];
[request setDelegate:self];
[request setDidFinishSelector:#selector(requestDone:)];
[request setDidFailSelector:#selector(requestWentWrong:)];
This request is currently made on every key press a user makes in a searchbox (The text typed is sent off in the request as the search string). However, rather than sending the request on every key press, I want to delay the request by a second to allow users to type further characters into the searchbox before the request is sent.
I've successfully made a thread that waits a second as users continue to type (although admittedly Im not convinced this is the best way to do it yet, but it works for now)...
this
[self performSelectorInBackground:#selector(wait:) withObject:request];
calls this
-(void)wait:(NSString *)request
{
[NSThread sleepForTimeInterval:1.00];
[[self queue] addOperation:request]; //queue is an NSOperationQueue
}
but, if a user continues to type, I haven't managed to work out how to cancel the request or not put the request in the queue, or empty the queue and replace it with the new request.
Finally, obviously I could force users to wait until they have pressed the 'search' button on the pop-up keyboard, but I was hoping to provide search results without that.
Thanks
The answer was to create an NSTimer, and invalidate it whenever a new key press had been made. Then start it again.
[timer invalidate];
You can try this to cancel
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(id)anArgument