I have a UITableView that gets JSON data from a Twitter search. I'm using polling to auto-refresh the view.
In my viewDidLoad, I have:
[NSTimer scheduledTimerWithTimeInterval:10.0 target:self selector:#selector(autoTweets:) userInfo:nil repeats:YES];
and then I have:
-(void) autoTweets : (NSTimer *)theTweets
{
NSURL *url = [NSURL URLWithString:#"http://search.twitter.com/search.json?q=%23hatlive%20-%22RT%20%40%22"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
connection = [NSURLConnection connectionWithRequest:request delegate:self];
if (connection)
{
webData = [[NSMutableData alloc] init];
}
[[self myTableView]setDelegate:self];
[[self myTableView]setDataSource:self];
array = [[NSMutableArray alloc] init];
}
Doing this refreshes the data every 10 seconds. The problem is, if I scroll on the 10th second when the code is executed, the app just crashes. If I don't scroll, it updates just fine. Does anyone know why?
Assuming array is the data used by the table view's data source, then the problem is that you change the contents of the array at the start of the connection. Then the contents get updated as the connection completes in the background. Meanwhile, as the table is scrolling, it is still trying to access the old contents of the array.
One solution is to setup a temporary array used to deal with the new connection. Don't update the old array with the new array until just before you call reloadData on the table.
Try [self.tableView beginUpdates]; and [self.tableView endUpdates]; while updating your data. Also you dont have to reset the datasource and delegate all the time. You may even call [self.tableView reloadData];.
You can disable touch on the table view right before the refresh, then enable after the data is set.
self.myTableView.userInteractionEnabled = YES; // before refresh
self.myTableView.userInteractionEnabled = NO; // after
Related
I am trying to create a loose version of LazyTabelImages using storyboard and JSON. in ViewDidLoad on my main TableViewController, I start an NSURLConnection to get the JSON data, but my cells do not load until after the connection is completed. I want the same behavior that LazyTableImages has, where the cells load as blanks, but then have the information filled in (reload the table data). I can duplicate this if I do not use storyboard, as LazyTables does not use storyboard, but that is not an option.
I have looked through LazyTableImages to try to find the solution, but storyboard make a big difference (to me anyway).
Is there a simple way to get the cells to load as blanks? For example, if the device has no internet, I still want my TableView to show up, and I will put a custom message in the cell.
Code:
The part of my viewDidLoad where I initialize the connection....
NSURLRequest *urlrequest = [NSURLRequest requestWithURL:[NSURL URLWithString:serverURL]];
self.dataConnection = [[NSURLConnection alloc] initWithRequest:urlrequest delegate:self];
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
connectionDidFinnishLoading...
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
//ListData below is an array that my data received (JSON) is loaded into. It is then passed to getTableData.
self.dataConnection = nil;
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self performSelectorOnMainThread:#selector(getTableData:) withObject:ListData waitUntilDone:YES];
});
}
getTableData...
-(void)getTableData:(NSData *)jsonData
{
NSError *error = nil;
arrayEntries = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableLeaves error:&error];
for (int x = 0; x < arrayEntries.count; x++)
{
NSMutableDictionary *dic = [arrayEntries objectAtIndex:x];
//ARecord is a class just like in LazyTableImages that creates objects to keep the icons/data together. The ARecords are loaded into the TableView
ARecord *arecord = [[ARecord alloc] init];
NSString *title = [dic objectForKey:#"title"];
NSString *subt = [dic objectForKey:#"subtitle"];
NSString *url = [dic objectForKey:#"image_URL"];
arecord.Icon = nil;
arecord.URL = url;
arecord.Name = title;
arecord.title = subt;
//this is where I load an array full of the arecord objects.
[array addObject:arecord];
}
[self.tableView reloadData];
}
I've done something similar. In viewDidLoad: I set the array for table data to a few objects of [NSNull null] for however many blank rows I want to show while the data is downloading. In cellForRowAtIndexPath: I check if [self.arrayOfTableData objectAtIndex:indexPath.row] = [NSNull null]. If so return a "blank" cell, otherwise load the cell with ARRecrod data.
Then when the URL completes, replace the array of NSNulls with array of your ARRecords.
I do this with two objects. First, I have an image fetcher class that downloads data asynchronously and notifies a delegate when it's complete. Then I have an image view class that implements the fetcher's delegate methods. So something like:
#implementation AsyncImageFetcher
-(id)initWithURL:(NSURL *)aURL andDelegate:(id<SomeProtocol>)aDelegate{
//...
NSURLRequest *req = [NSURLRequest requestWithURL:aURL];
//Note that NSURLConnection retains its delegate until the connection
//terminates! See comments in MyImageView below.
[NSURLConnection connectionWithRequest:req delegate:self];
//...
}
//Implement standard connection delegates here. The important part is:
-(void)connectionDidFinishLoading:(NSURLConnection *)connection{
// ...
UIImage *anImage = [self decodeDownloadedDataIntoAnImage];
if([[self delegate] respondsToSelector:#selector(imageFetcher:didFetchImage:)]){
[[self delegate] imageFetcher:self didFetchImage:anImage];
}
//...
}
#end
Then I subclass UIImageView or UIView or something (depending on how flexible you need to be) to implement the delegate protocol and fire off the fetcher:
#implementation MyImageView
-(id)initWithURL:(NSURL *)aURL andPlaceHolderImage:(UIImage *)aPlaceHolder{
//...
[self setImage:aPlaceHolder];
//Note we don't assign this to an ivar or retain it or anything.
//That's ok because the NSURLConnection inside the fetcher actually
//retains the fetcher itself. So it will live until the connection
//terminates -- which is exactly what we want. Thus we disable
//CLANG's noisy warnings.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-value"
[[AsyncImageFetcher alloc] initWithURL:aURL andDelegate:self];
#pragma clang diagnostic pop
return self;
}
-(void)imageFetcher:(MCMAsyncImageFetcher *)anImageFetcher didFetchImage:(UIImage *)anImage{
[self setImage:anImage];
}
#end
In your specific case, you'd just set a MyImageView as your cell's imageView in -tableView:cellForRowAtIndexPath:, passing reasonable values for its placeholder and URL, of course.
Since I haven't see your code, I just give my suggestion here:
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_queue_t queue = dispatch_queue_create(NULL, NULL);
dispatch_async(queue, ^{
//add your connection code here
//parse the json and store the data
//
dispatch_async(dispatch_get_main_queue(), ^{
//here to reload your table view again,
//since UI related method should run on main thread.
[YOUR_TABLEVIEW reloadData];
});
});
[YOUR_TABLEVIEW reloadData];
}
Note: Make sure your tableview in storyboard has connected to that in code! Hope it helps!
Hey guys I have done lots of work with ASIHTTPRequest so far and use it several times in my ios application, however in one area of my app where I have added another asihttprequest method its not working properly.
First of all it must be said that the reason I'm trying to spread the load of downloading and parsing the data.. If I do this on the subview it takes a good 2-3 second to go through the second xml sheet and get all of the related values out.. where as If I do it on this mainview where people are not seeing anything load etc then when the go back to the subview it should look almost instant. I don't know how correct this is but I figure its a okay thing to do to make the app feel abit snappier.
So I am setting it the asihttprequest methods identically as the other ones that work minus caching.
What happens is I select a table cell from the main view that loads the second view and parses a bunch of info to the tableview. the User then selects a value which is passed back to the main view and displayed.
I then parse another lot of xml data checking the selected valueID against everything in the second xml sheet. so that when the user selects the second cell I pass all the data that was just parsed over to the second view to make it look as though its loaded alot faster.
Heres a flow chart of that will explain what I'm trying to do abit better
This is what the parser code looks like in the main view which is the one thats working in the emulator but not on the iphone.
This is my protocol that I call from the subview and pass all the values I need to over and fire off the request to the mainviews ASIHTTPRequest.
- (void) setManufactureSearchFields:(NSArray *)arrayValues withIndexPath:(NSIndexPath *)myIndexPath
{
manufactureSearchObjectString = [[arrayValues valueForKey:#"MANUFACTURER"] objectAtIndex:0];
manufactureIdString = [[arrayValues valueForKey:#"MANUID"] objectAtIndex:0]; //Restricts Models dataset
manufactureResultIndexPath = myIndexPath;
[self.tableView reloadData]; //reloads the tabels so you can see the value in the tableViewCell.
//need some sort of if statment here so that if the back button is pressed modelSearchObjectString is not changed..
if (oldManufactureSearchObjectString != manufactureSearchObjectString) {
modelResultIndexPath = NULL;
modelSearchObjectString = #"empty";
oldManufactureSearchObjectString = [[NSString alloc] initWithFormat:manufactureSearchObjectString];
}
//These two lines below are what execute ASIHTTPRequest and set up my parser etc
dataSetToParse = #"ICMod"; // This sets the if statment inside parserDidEndDocument
[self setRequestString:#"ICMod.xml"]; //Sets the urlstring for XML inside setRequestString
}
This then fires the ASIHTTPRequest delegate methods.
- (IBAction)setRequestString:(NSString *)string
{
//Set database address
//NSMutableString *databaseURL = [[NSMutableString alloc] initWithString:#"http://127.0.0.1:8888/codeData/"]; // imac development
NSMutableString *databaseURL = [[NSMutableString alloc] initWithString:#"http://127.0.0.1:8888/codeData/"]; // iphone development
//PHP file name is being set from the parent view
[databaseURL appendString:string];
NSLog(#"%#", databaseURL);
//call ASIHTTP delegates (Used to connect to database)
NSURL *url = [NSURL URLWithString:databaseURL];
//This sets up all other request
ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
[request setDelegate:self];
[request startAsynchronous];
}
When I run this through debug with break points while testing on the iphone this is where the app falls over.. but On the emulator it has no problems.
This next method never gets called when testing on the iphone but workds sweet on the emulator.
- (void)requestFinished:(ASIHTTPRequest *)request
{
responseString = [request responseString]; //Pass requested text from server over to NSString
capturedResponseData = [responseString dataUsingEncoding:NSUTF8StringEncoding];
[self startTheParsingProcess:capturedResponseData];
}
This is the only other delegate that is fired when testing on the iphone, sends me an alret saying the connection has timed out.
- (void)requestFailed:(ASIHTTPRequest *)request
{
NSError *error = [request error];
NSLog(#"%#", error);
UIAlertView *errorAlert = [[UIAlertView alloc] initWithTitle:#"Error!" message:#"A connection failure occurred." delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
[errorAlert show];
}
I don't think you need to see all the parser delegates as I don't think they are the issue as this is where the app falls over...
here is what gets printed to the log...
2011-11-29 14:38:08.348 code[1641:707] http://000.000.000.000:0000/codeData/second.xml
2011-11-29 14:38:18.470 code[1641:707] Error Domain=ASIHTTPRequestErrorDomain Code=2 "The request timed out" UserInfo=0x1e83a0 {NSLocalizedDescription=The request timed out}
If you need more of my code let me know.. but I'm at abit of a loss here as like I say there is no difference to how Im doing this ASIHTTPRequest to other views other than I'm initializing it from the protocol that I'm setting up from the second view.. maybe I should set the values before I reload the table or something... I'm not sure though hopefully someone can help me out with this one and spot the issue I cannot see.
Can you view 'http://127.0.0.1:8888/codeData/' with Safari on the iPhone? Chances are that server isn't available from whatever networks the iPhone is connected to.
If your iMac is using DCHP it is possible that the address has changed since you originally set the value.
Are you sure you have the case correct on the URL? The simulator is much more forgiving on case-sensitivity than the actual device.
Do not fly blind but use an HTTP Proxy like Charles for making sure your requests are actually fired and result into what you expect.
A brief over view of what I am trying to do.
I am using the tableView:didSelectRowAtIndexPath: method inside my UITableViewController subclass which is catching a row selection from that view like so...
//..... inside tableView:didSelectRowAtIndexPath:
if (indexPath.section == 0) {
//--- Get the subview ready for use
VehicleSearchResponseTableViewController *vehicleSearchResponseTableViewController = [[VehicleSearchResponseTableViewController alloc] initWithNibName:#"VehicleSearchResponseTableViewController" bundle:nil];
// ...
//--- Sets the back button for the new view that loads
self.navigationItem.backBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:#"Back" style: UIBarButtonItemStyleBordered target:nil action:nil] autorelease];
// Pass the selected object to the new view controller.
[self.navigationController pushViewController:vehicleSearchResponseTableViewController animated:YES];
if(indexPath.row == 0) {
vehicleSearchResponseTableViewController.title = #"Mans";
EngineRequests *engineRequest = [[EngineRequests alloc] init];
[engineRequest getMans];
[engineRequest release];
}
if(indexPath.row == 1) {
//.... etc etc
As you can see in this method I set up a few things, pushing the new view onto the viewstack and changing the back buttons text, then I go into catching the different rows and then initiating a method in a subclass of nsobject where I want to have all my connection/request stuff going on.
Inside my NSObject I have several different methods for the different cells that you can select on the UITableViewController, basicly they specify different strings that will then initialize my ASIHTTPRequest wrapper to make a connection to the php script and catch all the data that will come back from the database.. NSObject looks like this.
//.... NSObject.m
- (IBAction) getMans
{
NSString *mansString = [[NSString alloc] initWithString:#"mans.php"];
[self grabURLInBackground:mansString];
[manusString release];
}
//....cont....
//--- Connect to server and send request ---------------->>
- (IBAction)grabURLInBackground:(NSString *)setUrlString
{
NSString *startURL = [NSString stringWithFormat:#"http://127.0.0.1:8888/CodeTest/%#", setUrlString];
NSURL *url = [NSURL URLWithString:startURL];
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:url];
[request setDelegate:self];
[request startAsynchronous];
}
- (void)requestFinished:(ASIHTTPRequest *)request
{
NSString *responseString = [request responseString]; //Pass request text from server over to NSString
NSData *responseData = [responseString dataUsingEncoding:NSUTF8StringEncoding]; //Create NSData object for Parser Delegate and load with responseString
NSLog(#"stuff %#",responseData);
}
- (void)requestFailed:(ASIHTTPRequest *)request
{
NSError *error = [request error];
NSLog(#"%#", error);
}
From here I would like to pass the data I am getting from the requestFinished method back over to the newly pushed UITableView.. However I have an error before I am able to get this far that I need to solve... if I run the simulator and click back and forth between the views (the main UITableViewController with the cells and then the newly popped view where I want to put the data) the application falls over and pops up an error in main.m Thread 1: program receive signal EXC_BAD_ACCESS.. I just don;t know whats causing because from what I can tell my code is not so bad.
Also when I debug my application I notice that once grabURLInBackground method has finished it bounces out back to the getMans method then goes back over to the UITableViewController and continues through the if statements, completely neglecting the requestFinished and requestFailed methods, and I just cannot figure out why.
I guess I am not sure if I am calling the methods and functions I need to use in the right places so if you have any suggestions or answers on how I can improve or if you know where my error is coming form that would be greatly appreciated.
There's a few issues with the code above but I'd guess that your bad access exception is due to the handling of your EngineRequests and use of AsiHttpRequest.
The code here
EngineRequests *engineRequest = [[EngineRequests alloc] init];
[engineRequest getMans];
[engineRequest release];
effectively creates an object then deallocates as soon as getMans has finished running.
Then inside the engineRequest object this code
[request setDelegate:self];
[request startAsynchronous];
requests that AsiHttpRequest notify the almost certainly released object once the request has completed.
There may be other issues at work here but I'd start by restructuring to try to keep this object around until at least after it's received the response from AsiHttpRequest.
Hard to tell from the brief overview, but generally when you bad_access and end up in the main application method, it's usually because you autoreleased something, then released it, and it craps out when the autorelease pool is drained. Might want to turn on NSZombiesEnabled and look for memory problems.
Who does receive your request?
The sender (and receiver) object is engineRequest.
But you release Engine Request in that very moment after you issued the async request (by mens of the getMans Method.
I would suggest that you
1. move the code
vehicleSearchResponseTableViewController.title = #"Mans";
EngineRequests *engineRequest = [[EngineRequests alloc] init];
[engineRequest getMans];
[engineRequest release];
from your UITableViewController's didSelectRowAtIndexPath method to your vehicleSearchResponseTableViewController's viewDidLoad method.
2. to retain your EngineRequests object and keep it in some instance variable within vehicleSearchResponseTableViewControllerand do not release it before the request is completely processed, either successfully or in error.
I'm developing an iPhone application where the data comes from a webservice that returns a json file. I'm using the following command in the viewDidLoad of the UITableViewController where I want to bind the data:
responseData = [[NSMutableData data] retain];
NSURLRequest *request = [NSURLRequest requestWithURL:
[NSURL URLWithString:#"webservice url"]];
[[NSURLConnection alloc] initWithRequest:request delegate:self];
and then I get the data from the json result into a NSDictionary in the connectionDidFinishedLodading Delegate.
The problem is that the numberOfRowsInSection delegate from UITableView executes before the connection finishes downloading data, which makes my table view with zero rows and doesn't show anything.
What am I doing wrong? Did I misunderstand the concept of the viewDidLoad and should be loading this data elsewhere?
Call reloadData on the tableView when you are done processing the data.
[self.tableView reloadData];
This triggers all UITableViewDatasource methods anew.
I have a strange issue, when it comes to parsing XML with NSXMLParser on the iPhone. When starting the app, I want to preload 4 table-views, that are populated by RSS-Feeds in the background.
When I init the table-views one-by-one, than loading, parsing and displaying all works like a charm. But when I try to init all view at once (at the same time), than it seems, that the XML-parser-instances are disturbing each other. Somehow data from one XML-Feed are "broadcasted" into other xml-parser instances, where they do not belong. Example: there is a "teammember" item, with "This is my name". When this bug occurs, there is a string from another xml-feed added, i.e. resulting in: "This is my name58", where 58 is the chart-position of something from the other view. "58" seems to miss then on the other instance.
It looks to me, that this bug occurs because of the NSXMLParser-delegate method:
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
if (!currentStringValue) {
currentStringValue = [[NSMutableString alloc] initWithCapacity:50];
}
[currentStringValue appendString:string];
}
In this case "by coincidence" bytes are appended to strings, where they do not belong to.
The strange thing is, that every instance of NSXMLParser is unique, got its own unique delegates, that are attached to their own ViewController. Every parsing-requests spawns it own background-task, with its own (also also unique named) Autorelease-pool.
I am calling the NSXMLParser like this in the ViewController:
// prepare XML saving and parsing
currentStringValue = [[[NSMutableString alloc] initWithCapacity:50] retain];
charts = [[NSMutableArray alloc] init];
NSURL *url = [[NSURL alloc] initWithString:#"http://(SOME XML URL)"];
xmlParser = [[[NSXMLParser alloc] initWithContentsOfURL:url] retain];
//Set delegate
[xmlParser setDelegate:self];
//loading indicator
progressWheel = [[[UIActivityIndicatorView alloc] initWithFrame:CGRectMake(150.0,170.0,20.0,20.0)] autorelease];
progressWheel.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray;
[self.view addSubview:progressWheel];
[progressWheel startAnimating];
// start loading and parsing the xml-feed in the background
//[self performSelectorInBackground:#selector(parse:) withObject:xmlParser]; -> I also tried this
[NSThread detachNewThreadSelector:#selector(parse:) toTarget:self withObject:xmlParser];
And this is one of the background-tasks, parsing the feed:
-(void)parse:(NSXMLParser*)myParser {
NSAutoreleasePool *schedulePool = [[NSAutoreleasePool alloc] init];
BOOL success = [myParser parse];
if(success) {
NSLog(#"No Errors. xmlParser got: %#", myParser);
(POST-PROCESSING DETAILS OF THE DATA RETURNED)
[self.tableView reloadData];
} else {
NSLog(#"Couldn't initalize XMLparser");
}
[progressWheel stopAnimating];
[schedulePool drain];
[myParser release];
}
What could cause this issue? Am I calling the background-task in the right way? Why is this bug approaching, since every XML-Parser got its own, unique instance?
You should not be updating UI elements (like progressWheel) from inside a background thread. UI updates should be done on the main thread.
Use -performSelectorOnMainThread:withObject:waitUntilDone: to update UI elements from within a background thread.
I've released an open source RSS/Atom Parser for iPhone and it makes reading and parsing web feeds extremely easy.
You can set it to download the data asynchronously, or you could run it in a background thread synchronously to collect the feed data.
Hope this helps!