I'm looking for a means to handle separate but related NSURLRequest and thought that I could add them to an NSOperationQueue and then manage them (run the request or not based on http status code - if the status code is 200 they can run, if not, stop all of them as the url string needs to be appended).
In my test code below I suspend the OQ to stop the processing of NSURLRequest (represented here by some public RSS feeds) but continue to the request to the OQ. I get the right number of operations (4). After adding all request to the OQ I then check to see if it has been suspended and if so, cancel all the operations.That works, at least the check if it has been suspended.
When I do a count check after canceling the operations I still get 4 but was expecting less (and hoping for 0). I'm using NSURLConnection to get the rss data in a NSObject subclass.
I understand from the docs that NSOQ will not remove an operation until it has reported that it is finished. (Is there a way to see this report?)
You cannot directly remove an operation from a queue after it has been added. An operation remains in its queue until it reports that it is finished with its task. Finishing its task does not necessarily mean that the operation performed that task to completion. An operation can also be canceled. Canceling an operation object leaves the object in the queue but notifies the object that it should abort its task as quickly as possible.
NSURLConnection doesn't have a willStart or similar delegate method so I can't track that but my feeling is the second RSS feed is in some sort of start process and that would explain why it is still in there. But I log the connectionDidFinishLoading delegate and so the first task is completed, so I was expecting at least that to be gone.
So my question is twofold.
1. If I nil out NSOQ, does that eliminate the operations within it? And what danger is there if one of those operations is in process - crash, hanging the app, etc?
2. Is there a way to cancel a NSURLConnection that is in process? (Assuming that the answer to 1 is yes, you are in the danger zone).
Here's my code:
- (void)viewDidLoad {
[super viewDidLoad];
connectionManager* myConnectionManager = [[connectionManager alloc] init];
NSOperationQueue* operationQueue = [[NSOperationQueue alloc] init];
NSMutableArray* arrAddedOperations = [[NSMutableArray alloc] init];
NSArray* arrFeeds = [[NSArray alloc] initWithObjects:#"http://rss.cnn.com/rss/cnn_topstories.rss", #"http://hosted.ap.org/lineups/USHEADS-rss_2.0.xml?SITE=RANDOM&SECTION=HOME", #"http://feeds.reuters.com/reuters/topNews", #"http://newsrss.bbc.co.uk/rss/newsonline_world_edition/americas/rss.xml", nil];
//add operations to operation queue
for(int i=0; i<arrFeeds.count; i++) {
NSInvocationOperation* rssOperation = [[NSInvocationOperation alloc]
initWithTarget: myConnectionManager
selector:#selector(runConnection:)
object:[arrFeeds objectAtIndex:i]];
//check to put a suspension on the OQ
if (i>1) {
operationQueue.suspended = YES;
}
[operationQueue addOperation:rssOperation];
[arrAddedOperations addObject:[arrFeeds objectAtIndex:i]];
//incremental count to see operations being added to the queue - should be 4
NSLog(#"This is the number of operations added to the queue:%i", [operationQueue operationCount]);
}
if (operationQueue.suspended) {
//restart the OQ so we can cancel all the operations
operationQueue.suspended = NO;
//kill all the operations
[operationQueue cancelAllOperations];
//count to see how many operations are left
NSLog(#"OQ has been suspended and operations canclled. The operation count should be 0\nThe operation count is %i", [operationQueue operationCount]);
}
}
from NSURLConnection class
- (void) runConnection : (NSString*) strURL {
NSURLRequest* urlRequest = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:strURL]];
self.myConnection = [[NSURLConnection alloc] initWithRequest:urlRequest delegate:self startImmediately:NO];
[self.myConnection setDelegateQueue:self.myQueue];
[self.myConnection start];
self.myConnection = nil;
}
#pragma mark - NSURLConnection Delegates
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(#"%#", error.localizedDescription);
}
- (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
NSLog(#"%#", [NSNumber numberWithInteger:httpResponse.statusCode]);
}
- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
self.strReponse = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
//NSLog(#"%#", self.strReponse);
}
- (void) connectionDidFinishLoading:(NSURLConnection *)connection {
NSLog(#"task finished");
NSDictionary* dictUserInfo = [[NSDictionary alloc] initWithObjectsAndKeys:
#"Display Data", #"Action",
self.strReponse, #"Data",
nil];
[[NSNotificationCenter defaultCenter] postNotificationName:#"avc" object:self userInfo:dictUserInfo];
}
Edit: I don't need to save these operations as I am storing the incoming request in a mutable array and which just create a new OQ once they have been appended. I just want to make sure they are cancelled and not leaving the app in a fragile state.
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'm trying to get data from a website to display it inside a table view
My code:
-(void)loadTutorials {
NSURL *url = [NSURL URLWithString:[#"http://www.example.com/search?q=" stringByAppendingString:self.searchString]];
NSURLRequest *UrlString = [[NSURLRequest alloc] initWithURL:url];
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:UrlString
delegate:self];
[connection start];
NSLog(#"Started");
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
TFHpple *tutorialsParser = [TFHpple hppleWithHTMLData:data];
NSString *tutorialsXpathQueryString = #"//div[#id='header']/div[#class='window']/div[#class='item']/div[#class='title']/a";
NSArray *tutorialsNodes = [tutorialsParser searchWithXPathQuery:tutorialsXpathQueryString];
NSMutableArray *newTutorials = [[NSMutableArray alloc] init];
for (TFHppleElement *element in tutorialsNodes) {
Data *tutorial = [[Data alloc] initWithTitle: [[element firstChild] content]
Url: [#"http://www.example.com" stringByAppendingString: [element objectForKey:#"href"]]
];
[newTutorials addObject:tutorial];
}
_objects = newTutorials;
[self.tableView reloadData];
}
but the data is not showing up, is the data not finished loading?
I got it to working without NSURLConnection but then it will stop the program until the data is recieved
According to NSURLConnectionDataDelegate
connection:didReceiveData:
is called in a incrementally manner.
The newly available data. The delegate should concatenate the contents
of each data object delivered to build up the complete data for a URL
load.
So this means you should append new data within this method.
Then, in
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
you should manipulate your data.
So, for example
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
// Create space for containing incoming data
// This method may be called more than once if you're getting a multi-part mime
// message and will be called once there's enough date to create the response object
// Hence do a check if _responseData already there
_responseData = [[NSMutableData alloc] init];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// Append the new data
[_responseData appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
// Parse the stuff in your instance variable now
}
Obviously you should also implement the delegate responsible for error handling.
A simple note is the following. If data is too big and you need to do some computations stuff (e.g. parsing), you could block the UI. So, you could move the parsing in a different thread (GCD is your friend). Then when finished you could reload the table in the main thread.
Take a look also here for further info: NSURLConnectionDataDelegate order of functions.
My app goes to a viewcontroller, makes two automatic server requests, makes the connection, retrieves the data and correctly displays it, and is done. The user clicks a "likes" button and two more server requests are made - successfully. Displays are correct. Should be done. Then it crashes, with the error:
[__NSCFNumber isEqualToString:]: unrecognized selector sent to instance
I'm using the very handy SimplePost class (by Nicolas Goles). Here are my requests, which are both called in viewDidLoad:
- (void) setScore {
Profile *newPf = [[Profile alloc] initID:thisUser profil:#"na" scor:score];
NSMutableURLRequest *reqPost = [SimplePost urlencodedRequestWithURL:[NSURL URLWithString:kMyProfileURL] andDataDictionary:[newPf toDictPf]];
(void) [[NSURLConnection alloc] initWithRequest:reqPost delegate:self];
}
- (void) saveHist {
History *newH = [[History alloc] initHistID:thisUser hQid:thisQstn hPts:score hLiked:NO];
NSMutableURLRequest *reqHpost = [SimplePost urlencodedRequestWithURL:[NSURL URLWithString:kMyHistURL] andDataDictionary:[newH toDictH]];
(void) [[NSURLConnection alloc] initWithRequest:reqHpost delegate:self];
}
The only "new" thing with my custom classes (Profile and History) is the BOOL for hLiked, but it's "working" - the database is updating correctly.
Then, the user can click a "Likes" button (+ or -). Here are the other requests:
- (IBAction)likeClick:(id)sender {
double stepperValue = _likeStepper.value;
_likesLbl.text = [NSString stringWithFormat:#"%.f", stepperValue];
[self updateLikes];
[self updateHist];
}
- (void) updateLikes {
// update the question with the new "liked" score
NSInteger likesN = [_likesLbl.text integerValue];
Questn *qInfo = [[Questn alloc] initQwID:thisQstn askID:0 wCat:#"na" wSit:#"na" wAns1:#"na" wPts1:0 wAns2:#"na" wPts2:0 wAns3:#"na" wPts3:0 wAns4:#"na" wPts4:0 wJust:#"na" wLikes:likesN ];
NSMutableURLRequest *reqPost = [SimplePost urlencodedRequestWithURL:[NSURL URLWithString:kLikesURL] andDataDictionary:[qInfo toDictQ]];
(void) [[NSURLConnection alloc] initWithRequest:reqPost delegate:self];
}
- (void) updateHist {
History *newH = [[History alloc] initHistID:thisUser hQid:thisQstn hPts:98989 hLiked:YES];
NSMutableURLRequest *reqHpost = [SimplePost urlencodedRequestWithURL:[NSURL URLWithString:kHistURL] andDataDictionary:[newH toDictH]];
(void) [[NSURLConnection alloc] initWithRequest:reqHpost delegate:self];
}
Messy, right? Here's my connection code:
// connection to URL finished with Plist-formatted user data array returned from PHP
- (void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSDictionary *array = (NSDictionary *)[NSPropertyListSerialization propertyListFromData:data mutabilityOption:NSPropertyListImmutable format:0 errorDescription:nil];
BOOL keyLikeExists = [array objectForKey:#"likes"] != nil;
if( keyLikeExists ) {
_likesLbl.text = [array objectForKey:#"likes"];
}
}
- (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
NSLog(#"Connection did fail." );
}
It all does a good job, and then a couple of seconds later it crashes with that "unrecognized selector" error mentioned above, like there's still some URL activity happening. There shouldn't be.
Anybody seen this kind of thing before? Many thanks for any help!
Somewhere in your code there's a call to the method isEqualToString:. The thing that's being sent that message is a NSNumber object rather than a string. Either there's a logic problem concerning the object type or there's a memory problem where a string was over-released and its memory is being re-used to hold a number.
Without seeing the context for the call, it's hard to guess.
If you break on the exception, the stack trace should tell you where in the code it's failing.
I'm trying to pull images from the server for the scrollview. After the user zooms the view in or out, the image should be downloaded:
- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale {
Ymin=365000+375000*_scrollView.contentOffset.x/(scale*1024);
Ymax=365000+375000*(_scrollView.contentOffset.x/scale+1024/scale)/1024;
Xmin=6635000-260000*(_scrollView.contentOffset.y/scale+748/scale)/748;
Xmax=6635000-260000*_scrollView.contentOffset.y/(scale*748);
[self looYhendus]; //Creates NSURLConnection and downloads the image according to scale, Ymin, Ymax, Xmin and Xmax values
UIImage *saadudPilt=_kaardiPilt;
imageView=[[UIImageView alloc] initWithImage:saadudPilt];
imageView.frame=CGRectMake(_scrollView.contentOffset.x,_scrollView.contentOffset.y,1024,748);
[_scrollView addSubview:imageView];
}
On some occasions (I can't figure out, on what conditions), it works, but on some occasions NSURLConnection delegate methods won't get fired and the image set as the subview is still the image that is initially downloaded (when the application launches). Then, only after I touch the screen again (the scrollview scrolls), the NSLog message shows that the image is downloaded. What could be the reason of this kind of a behaviour?
EDIT: Added the NSURLConnection delegate methods. I've tried a few other ways but they all end up not executing the delegate methods. Which made me think that it's not about NSURConnection but rather UIScrollView (obviously, I can be wrong about this).
- (void)looYhendus
{
yhendused=CFDictionaryCreateMutable(
kCFAllocatorDefault,
0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
NSString *aadress = [NSString stringWithFormat:#"http://xgis.maaamet.ee/wms-pub/alus?version=1.1.1&service=WMS&request=GetMap&layers=MA-ALUSKAART&styles=default&srs=EPSG:3301&BBOX=%d,%d,%d,%d&width=%d&height=%d&format=image/png",Ymin,Xmin,Ymax,Xmax,512,374];
NSURL *url = [[NSURL alloc] initWithString:aadress];
NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:url];
NSURLConnection *theConnection = [[NSURLConnection alloc] initWithRequest:theRequest delegate:self];
if( theConnection )
{
andmedServerist = [NSMutableData data];
CFDictionaryAddValue(
yhendused,
(__bridge void *)theConnection,
(__bridge_retained CFMutableDictionaryRef)[NSMutableDictionary
dictionaryWithObject:[NSMutableData data]
forKey:#"receivedData"]);
}
CFRunLoopRun();
}
-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response
{
[andmedServerist setLength: 0];
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSMutableDictionary *connectionInfo =
(NSMutableDictionary*)objc_unretainedObject(CFDictionaryGetValue(yhendused, (__bridge void *)connection));
[[connectionInfo objectForKey:#"receivedData"] appendData:data];
}
-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Ühenduse viga" message:#"Kõige tõenäolisemalt on kaardiserveril probleeme või puudub seadmel internetiühendus" delegate:self cancelButtonTitle:#"Sulge" otherButtonTitles:nil];
[alert show];
CFRunLoopStop(CFRunLoopGetCurrent());
}
-(void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSMutableDictionary *connectionInfo =
(NSMutableDictionary*)objc_unretainedObject(CFDictionaryGetValue(yhendused, (__bridge void *)connection));
[connectionInfo objectForKey:#"receivedData"];
andmedServerist=[connectionInfo objectForKey:#"receivedData"];
_kaardiPilt = [UIImage imageWithData: andmedServerist];
CFDictionaryRemoveValue(yhendused, (__bridge void *)connection);
CFRunLoopStop(CFRunLoopGetCurrent());
}
EDIT: added this:
- (void)viewDidLoad
{
[super viewDidLoad];
Ymin=365000;
Ymax=740000;
Xmin=6375000;
Xmax=6635000;
[self looYhendus];
UIImage *saadudPilt=_kaardiPilt;
imageView=[[UIImageView alloc] initWithImage:saadudPilt];
imageView.frame=CGRectMake(0,0,1024,748);
[_scrollView addSubview:imageView];
[_scrollView setContentSize: CGSizeMake(1024, 748)];
_scrollView.minimumZoomScale = 1.0;
_scrollView.maximumZoomScale = 50.0;
_scrollView.delegate = self;
}
Why are you explicitly calling CFRunLoopRun(); and stopping it in connectionDidFinishLoading: or didFailWithError: methods ? (CFRunLoopStop(CFRunLoopGetCurrent());)
Assuming you are doing this on main thread. You are stopping mainthread's runloop. There can be timers, ScrollView uses events which stop responding because you stopped the main thread's runloop.
If you are calling NSURLConnection on the main thread you don't need to explicitly run it (or stop it). You can just schedule to run on current runloop which is main threads runloop. If you are doing it on a background thread, then your code seems valid (Although you shouldn't show UIAlertView in didFailWithError: if its called on separate thread).
Updated Answer(Based on #Shiim's comment):
In the viewDidLoad method you are calling [self looYhendus]; which returns immediately (as you are using Asynchronous URL Connection for the load). So its working as expected. Move [scrollView addSubview:imageView] to connectionDidFinishLoading: method which would add your downloaded imageView data to scrollView's subView once finished downloading it. Or you can consider using dispatch_queue's to create a thread and load the URL request synchronously then using dispatch_queue's main queue to dispatch drawing of imageView added as subView to ScrollView onto main thread.
My recommendation in your case would be redesigned approach using dispatch_queue's. Which would be give you better understanding of solving problem (in this scenario) and also improves your code readability.
I had the same problem recently. The issue was that I was putting my connection in an asynchronous thread, when connections are already asynchronous.
I found the solution here: NSURLConnection delegate methods are not called
There are also a few links to other people who had similar issues in that thread.
If I were you though, I would try simply using [theConnection start] and initializing the request with a set timeout so you don't have to worry about the background thread shutting out before the image is downloaded.
For example:
[request setTimeoutInterval:30];
I have an issue on how to refresh the UI for iOS apps. What I wanted to achieve is this:
Show data in UITableView based on data retrieved from web service
The web service should be called from a separate thread (not main thread)
After the data is retrieved, it will refresh the contents of UITableView with the retrieved data
It is due so that the UI will not hang or the app will not block user input while in the process of receiving data from the web service in bad network connection
To do that, I create the following source code:
- (void)viewDidLoad
{
[super viewDidLoad];
NSURL *myURL = [[NSURL alloc] initWithString:[Constant webserviceURL]];
NSURLRequest *request = [NSURLRequest requestWithURL:myURL cachePolicy:NSURLRequestReloadIgnoringLocalAndRemoteCacheData timeoutInterval:60];
[[NSURLConnection alloc] initWithRequest:request delegate:self];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data{
NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
[self myparser] = [[MyXMLParser alloc] initXMLParser];
[parser setDelegate:myparser];
BOOL success = [parser parse];
if (success) {
// show XML data to UITableView
[_tableView performSelectorOnMainThread:#selector(reloadData) withObject:[myparser xmldata] waitUntilDone:NO];
}
else {
NSLog(#"Error parsing XML from web service");
}
}
==================
Is my implementation correct? Anybody know how to resolve it?
You would want to call
+ (void)sendAsynchronousRequest:(NSURLRequest *)request queue:(NSOperationQueue *)queue completionHandler:(void (^)(NSURLResponse*, NSData*, NSError*))handler
It will make the call to get the Data on a different thread then when the data pulled down or it had problems download data from the url it will call your handler block on the same thread as the original call was made.
Here is one way to use it: https://stackoverflow.com/a/9409737/1540822
You can also use
- (id)initWithRequest:(NSURLRequest *)request delegate:(id < NSURLConnectionDelegate >)delegate
And this will call one of your NSURLConnectionDelegate methods when data is downloaded in chucks. If you going to have large data then you may want to use this so that you don't spend too much time in the response.