I made a clone of UITableView called TableView with its own dataSource and delegate that mimics the original UITableView but is intended to do some things differently. I also made a GoogleSuggest class with its own delegate that requests google autocomplete suggestions from a known URL.
The GoogleSuggest class has this method:
- (void)requestSuggestionsForText:(NSString *)text {
[NSThread detachNewThreadSelector:#selector(asyncRequestSuggestionsForText:)
toTarget:self
withObject:text];
}
When called it dispatches this private background thread:
- (void)asyncRequestSuggestionsForText:(NSString *)text;
When it receives results it calls this delegate method:
- (void)googleSuggestDidReceiveResult:(GoogleSuggestResult *)result;
Everything worked fine with little controlled experiments until I put it all together in the main ViewController.
Initially, this method returned a "UI API called on a background thread" error:
#pragma mark - GoogleSuggestDelegate
- (void)googleSuggestDidReceiveResult:(GoogleSuggestResult *)result {
_googleSuggestions = result.suggestions;
[_tableView reloadData];
}
Then I replaced the last line with this and it worked:
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
Now, I'm getting a "-[__NSCFNumber length]: unrecognized selector sent to instance 0xbbe7252cd9143595" error.
The result.suggestions is a simple NSMutableArray with NSString variables, no NSNumbers anywhere.
This works and I get to see all results logged:
- (TableViewCell *)tableView:(tableView *)tableView cellAtIndex:(NSUInteger)index {
TableViewCell *cellView = [[TableViewCell alloc] init];
NSString *result = [_googleSuggestions objectAtIndex:index];
NSLog(#"Result: %#", result);
// cellView.titleLabel.text = result;
return cellView;
}
This also works and I get to see all results logged:
- (TableViewCell *)tableView:(tableView *)tableView cellAtIndex:(NSUInteger)index {
TableViewCell *cellView = [[TableViewCell alloc] init];
NSString *result = [_googleSuggestions objectAtIndex:index];
NSLog(#"Result: %#", result);
cellView.titleLabel.text = #"example text";
return cellView;
}
This fails when I try to assign the result to the titleLabel.text:
- (TableViewCell *)tableView:(tableView *)tableView cellAtIndex:(NSUInteger)index {
TableViewCell *cellView = [[TableViewCell alloc] init];
NSString *result = [_googleSuggestions objectAtIndex:index];
NSLog(#"Result: %#", result);
cellView.titleLabel.text = result;
return cellView;
}
It makes no sense, it's clearly an NSString variable assigned to an object that has no problem with NSString variables like shown in the working examples above.
How do you properly implement async search results?
How do you properly update UI elements from a background thread?
Related
I have dropped my code into a situation where I need to call UITableView data source methods written in some UIViewController class before a particular view is presented so that the cells get prepopulated and I can set a BOOL that the data in the not present viewController class is valid or not. I may explain it in more detail if required, but I wanted to know if its possible to do that. If yes, then how to do it? .. as a particular set of my code written after [tableView reloadData] is dependent on running the dataSource methods of UITableView. Please throw some light on this, if needs to be handled in a specific thread?
Following is the case where I call reloadData. Note: This is happening in another class when basicFactsViewController's viewWillAppear method has not been called yet:
- (BOOL) isComplete {
dispatch_async(dispatch_get_main_queue(), ^{
[basicFactsViewController.tableView reloadData];
});
return basicFactsViewController.isComplete && selectedVehicleId && selectedMakeId && selectedModelId && selectedYearId && selectedTrimId;
}
Now basicFactsViewController.isComplete is checked in this method:
- (BOOL) isComplete {
[self collectKeyHighlights];
return _isComplete;
}
Now the dictionary "tableCells" in the method below uses the cells population to check whether all features have been completed or not:
- (NSDictionary *) collectKeyHighlights {
NSMutableDictionary *key_highlights_update = [NSMutableDictionary new];
NSMutableDictionary *cell_highlight_update = [NSMutableDictionary new];
if(visible_key_highlights.count == 0) _isComplete = YES;
_isComplete = YES;
__block NSMutableArray *reloadCellAtIndexPathSet = [[NSMutableArray alloc] init];
[visible_key_highlights enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSDictionary *feature = (NSDictionary *)obj;
UITableViewCell *cell = [self.tableCells objectForKey:[NSIndexPath indexPathForRow:idx inSection:0]];
if(cell) {
if([cell isKindOfClass:[DRColorSelectionTableViewCell class]]) {
NSInteger selectedIndex = ((DRColorSelectionTableViewCell *)cell).selectedIndex;
NSInteger numberOfSegments = ((DRColorSelectionTableViewCell *)cell).numberOfSegments;
if(selectedIndex > -1 ) {
NSArray *dataValues = [[visible_key_highlights objectAtIndex:idx] objectForKey:#"data_values"];
NSDictionary *colorData;
BOOL reloadCellForIndexPath = NO;
if (numberOfSegments == selectedIndex) {
colorData = #{ #"normalized" : #"user_defined", #"isother" : #YES, #"hexcode":#"#FFFFFF", #"actual":((DRColorSelectionTableViewCell *)cell).otherColorTextField.text};
reloadCellForIndexPath = YES;
}
else{
colorData = [dataValues objectAtIndex:selectedIndex];
}
[key_highlights_update setObject:colorData forKey:[feature objectForKey:#"name"]];
[cell_highlight_update setObject:colorData forKey:[feature objectForKey:#"name"]];
if (![colorData isEqual:[prevSelections objectForKey:[feature objectForKey:#"name"]]]) {
[reloadCellAtIndexPathSet addObject:((DRColorSelectionTableViewCell *)cell).indexPath];
}
//if (reloadCellForIndexPath) {
//}
} else {
_isComplete = NO;
}
} else if([cell isKindOfClass:[DRInputTableViewCell class]]) {
NSString *textInput = ((DRInputTableViewCell *)cell).inputTextField.text;
if([textInput length]) {
[key_highlights_update setObject:[NSString toSnakeCase:textInput] forKey:[feature objectForKey:#"name"]];
[cell_highlight_update setObject:textInput forKey:[feature objectForKey:#"name"]];
}else {
_isComplete = NO;
}
} else if([cell isKindOfClass:[DRPickerTableViewCell class]]) {
NSString *textInput = ((DRPickerTableViewCell *)cell).inputField.text;
if([textInput length]) {
[key_highlights_update setObject:[NSString toSnakeCase:textInput] forKey:[feature objectForKey:#"name"]];
[cell_highlight_update setObject:textInput forKey:[feature objectForKey:#"name"]];
} else {
_isComplete = NO;
}
} else if([cell isKindOfClass:[DRSwitchTableViewCell class]]) {
// send this everytime for now
BOOL isSelected = ((DRSwitchTableViewCell *)cell).toggleButton.selected;
[key_highlights_update setObject:[NSNumber numberWithBool:isSelected] forKey:[feature objectForKey:#"name"]];
[cell_highlight_update setObject:[NSNumber numberWithBool:isSelected] forKey:[feature objectForKey:#"name"]];
}
}
else{
_isComplete = NO;
}
}];
prevSelections = cell_highlight_update;
if ([reloadCellAtIndexPathSet count]) {
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:reloadCellAtIndexPathSet withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
}
return key_highlights_update;
}
Now here since
[tableView reloadData]
is not calling cellForRowAtIndePath:, hence, tableCells is not getting populated, hence, I am always getting _isComplete = NO.
If I understand correctly, there is processing being done when the tableview loads (calls it's dataSource methods) and you want to trigger that early to use its results. Calling [basicFactsViewController.tableView reloadData]; early won't work if the basicFactsViewController hasn't been displayed yet. If basicFactsViewController is a UIViewController and has the default view and the tableView property is a subview of that standard view, then (if I remember correctly) the tableView property will be nil until the basicFactsViewController has been displayed. A shortcut around that is to access the viewController's view property and cause it to initialize (viewDidLoad and all that). You can do that by simply messaging the viewController: [basicFactsViewController view].
If I've been right so far I'm fairly confident that will initialize the tableView property. But I'm not sure if it will cause the table view to load its data. And even if it does work, it's definitely not the best solution to the piece of code you're trying to architect. Apple's design for UIKit has been focused on the model/view/controller pattern and it's easier to go with the flow and do the same. I imagine that you could move the processing that is in the data source methods for the tableView out into another class (or maybe even the same class), and call that method to get everything ready for both the tableView and any other checks that you have, storing the data in dictionaries and arrays in such a way that you can easily load them by index into the tableView when cellForIndex is called.
I am using parse as my database to store the text a user enters, and then display it onto the JSMessageViewController.
I am having difficulty understanding why my PFObject will not pass, and instead, I see empty message cells..
In my code here as you can see, if i pass either of the // code, it crashes, i know nil wont work and thats why i am getting the blank... but how do I pass a PFObject into the JSMessageData ?
- (id<JSMessageData>)messageForRowAtIndexPath:(NSIndexPath *)indexPath
{
//PFObject *chat = self.chats[(NSUInteger) indexPath.row];
//return chat;
//PFObject *chat = self.chats[indexPath.row];
//NSString *message = chat[kMMKChatTextKey];
//return message;
return nil;
}
JSMessageData has 3 instances : - (NSDate *)date, - (NSString *)sender, and - (NSString *)text ....
http://cocoadocs.org/docsets/JSMessagesViewController/4.0.0/Protocols/JSMessageData.html
Has anyone worked with this ? or can you help me figure out how I can pass the PFOject through - Parse is working fine, and the text entered, and after that when i press send, its stored in Parse.
-(void)didSendText:(NSString *)text fromSender:(NSString *)sender onDate:(NSDate *)date
{
if (text.length != 0){
PFObject *chat = [PFObject objectWithClassName:#"Chat"];
[chat setObject:self.chatRoom forKey:kMMKChatChatroomKey];
[chat setObject:self.currentUser forKey:kMMKChatFromUserKey];
[chat setObject:self.withUser forKey:kMMKChatToUserKey];
[chat setObject:text forKey:kMMKChatTextKey];
[chat saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
[self.chats addObject:chat];
//[JSMessageSoundEffect playMessageSentSound];
[self.tableView reloadData];
[self finishSend];
[self scrollToBottomAnimated:YES];
}];
}
}
use objectForKey method on PFObject, and pass appropriate key to retrieve text
The code below should do what you need.
- (id<JSMessageData>)messageForRowAtIndexPath:(NSIndexPath *)indexPath
{
PFObject *chat = self.chats[indexPath.row];
NSString *message = chat[kMMKChatTextKey];
JSMessage *jsMessage = [[JSMessage alloc] initWithText:message sender:nil date:nil];
return jsMessage;
}
I have a UITableView that uses paging. All the delegates, and datasource are set.
My table view fetches a list of ten cars over the network and displays them by sending a page number (currentPage). During this fetch request I also get the pageCount which is the number of pages that contains cars on the server. Each page contains 10 cars.
I create a loading cell on the row that equals self.allCars.count which is my car array. This cell then fetches the next ten, and adds them to the self.allCars.count array. A loading cell is then created again for self.allCars.count + 1 etc. (I hope you get the picture, if not please ask).
On first launch the list contains All Cars which is the default request. However, the user can change it from a drop down. For example, they can select Blue Cars. This is passed into the fetchCars methods as the params parameter.
There is an unwanted behaviour in my code however: When I scroll down through the list, with the default paramter selected, and I scroll down three pages (three network calls to fetchCars...) and the array now contains 30 cars displayed in the tableView. However I now want to start a different search from scratch, so I go to the drop down, and select to filter by only blue cars (donePickerBlue). This method removes all the car objects, sets the currentPage back to 1, calls the network for the blue cars, and reloads the data. The unwanted behaviour occurs here. Because there had been 30 cells/indexPath.rows, the network call is called 3 times. This is because the indexPath.row < self.allCars.count is not true. This is where I am stuck, I can't seem to figure out how to fix it, so that if the search parameter is change (blue in this case) that it should treat it as new, I thought the [tableView reloadData] would handle this, but unfortunately it remembers how many index paths there are.
Its something i've been stuck on for a while. I've a feeling im missing something very simple to fix it.
Header file
#property (nonatomic) NSInteger currentPage;
#property (nonatomic) NSInteger pageCount;
Implementation
-(void)viewDidLoad{
...
self.currentPage = 1;
...
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
if (self.allCars.count ==0) {
return 0;
}
else{
if (self.currentPage<self.pageCount)
return self.allCars.count+1;
}
return 0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell * cell = nil;
if (self.allCars.count!=0) {
if(indexPath.row <self.allCars.count){//here is where the problem occurs
cell=[self customCellForIndexPath:indexPath tableView:tableView];
}
else {
cell=[self loadingCell];
}
}
else{
// Disable user interaction for this cell.
cell = [[UITableViewCell alloc] init];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
return cell;
}
-(UITableViewCell *)loadingCell{
UITableViewCell * cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil];
UIActivityIndicatorView * activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
activityIndicator.center = cell.center;
cell.backgroundColor = [UIColor lightGrayColor];
[cell addSubview:activityIndicator];
cell.tag=kLoadingCellTag;
[activityIndicator startAnimating];
return cell;
}
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
if (cell.tag==kLoadingCellTag) {
self.currentPage++;
[self performSelector:#selector(getCars:withParams) withObject:nil afterDelay:1.5f];
}
}
-(void)getCars{
[self getCars:url withParams:params];
}
-(void)getCars: (NSURL *)url withParams: (NSString *)params{
NSMutableURLRequest * request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:0 timeoutInterval:80];
[request setHTTPBody:[params dataUsingEncoding:NSUTF8StringEncoding]];
[request setHTTPMethod:#"POST"];
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForResource=1;
NSURLSession * session = [NSURLSession sessionWithConfiguration:sessionConfig];
NSURLSessionDataTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse * httpResp = (NSHTTPURLResponse *)response;
NSDictionary * dataDict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
if (data) {
switch (httpResp.statusCode) {
case 200:{
dispatch_async(dispatch_get_main_queue(), ^{
self.pageCount = [dataDict[#"message"][#"total_pages"] intValue];
NSArray * carsArray = dataDict[#"message"][#"results"];
for (NSDictionary *cDict in carsArray) {
Car *car = [Car carWithID:[cDict[#"car_id"] stringValue] ];
car.car_name=cDict[#"car_name"];
car.car_description = cDict[#"car_description"];
[self.allCars addObject:car];
}
[self.tableView reloadData];
});
break;
}
default:
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Error");
});
break;
}
}
else{
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Error");
});
}
}];
[task resume];
}
//reset list to start new search
-(void)donePickingBlue{
[self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:YES];
self.currentPage=1;
[self.allCars removeAllObjects];
[self getCars:url withParams:blue];
}
Edit
I seem to have resolved the the problem by doing the following;
//reset list to start new search
-(void)donePickingBlue{
self.currentPage=1;
[self.allCars removeAllObjects];
[self.tableView reloadData];//after removing all the cars, now we call reload, as there are no cars. I was calling reload in `[self getCars:....]` just below, and thought this was enough.
[self getCars:url withParams:blue];
}
I was able to answer my own problem. The answer can be seen in the Edit above incase anybody else has the same problem.
It should have been;
//reset list to start new search
-(void)donePickingBlue{
self.currentPage=1;
[self.allCars removeAllObjects];
[self.tableView reloadData];//after removing all the cars, now we call reload, as there are no cars. I was calling reload in `[self getCars:....]` just below, and thought this was enough.
[self getCars:url withParams:blue];
}
If you want to download cars page by page, willDisplayCell: is pretty good choice. But you must change the condition a little, to prevent downloading the same data multiple times. Also, I recommend you to change data model and provide ability to determine a page for particular cars. That's what I mean:
-(void)tableView:(UITableView *)tableView
willDisplayCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath{
// 10 cells on page
NSUInteger currentPage = indexPath.row / 10;
// Check, if cars for the current page are downloaded
if (carsOnPagesDict[#(currentPage)] != nil) {
// Add a stub to indicate that downloading started
// You can use this later to display correct cell
// Also it prevents getCars: from calling multiple times for the current page
carsOnPagesDict[#(currentPage)] = #"downloading";
// I removed delay for simplicity
[self getCars:url withParams:params forPage:currentPage];
}
}
Also, change getCars method:
-(void)getCars:(NSURL *)url withParams:(NSString *)params forPage:(NSUInteger)page{
// Creating request...
// ...
// Processing response...
// ...
// Array obtained:
NSArray *carsArray = dataDict[#"message"][#"results"];
// Storing required data to the array
NSMutableArray *cars = [NSMutableArray arrayWithCapacity:carsArray.count];
for (NSDictionary *cDict in carsArray) {
Car *car = [Car carWithID:[cDict[#"car_id"] stringValue] ];
car.car_name=cDict[#"car_name"];
car.car_description = cDict[#"car_description"];
[cars addObject:car];
}
// Save cars to the dictionary for the page given
carsOnPagesDict[#(page)] = cars;
// ...
// Resuming tasks...
}
You may consider using CoreData to store that cars.
I have tableview where is name and status. Status is changed when come apple push notification (APNS).
But I have this problem. What can I do, if notification didn't come? Or if user tap on close button of this message.
I try to update table by using ASIHTTPRequest:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
HomePageTableCell *cell = (HomePageTableCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
// Configure the cell...
NSManagedObject *device = [self.devices objectAtIndex:indexPath.row];
cell.nameLabel.text = [device valueForKey:#"name"];
if ([[device valueForKey:#"status"] isEqualToNumber:#1])
{
cell.status.text = #"Not configured";
cell.stav.image = [UIImage imageNamed:#"not_configured.png"];
}
if ([[device valueForKey:#"status"] isEqualToNumber:#2])
{
//some other states
}
return cell;
}
I try this to change status before cell is loading...
- (void) getStatus:(NSString *)serialNumber
{
NSURL *url = [NSURL URLWithString:#"link to my server"];
__block ASIFormDataRequest *request = [ASIFormDataRequest requestWithURL:url];
__weak ASIHTTPRequest *request_b = request;
request.delegate = self;
[request setPostValue:#"updatedevice" forKey:#"cmd"];
[request setPostValue:serialNumber forKey:#"serial_number"]; //get status of this serial number
[request setCompletionBlock:^
{
if([self isViewLoaded])
{
[MBProgressHUD hideHUDForView:self.view animated:YES];
if([request_b responseStatusCode] != 200)
{
ShowErrorAlert(#"Comunication error", #"There was an error communicating with the server");
}
else
{
NSString *responseString = [request_b responseString];
SBJsonParser *parser = [[SBJsonParser alloc] init];
NSDictionary *result = [parser objectWithString:responseString error:nil];
status = [result objectForKey:#"status"];
NSInteger statusInt = [status intValue]; //change to int value
//here I want to change cell status in SQLite, but don't know how
//something with indexPath.row? valueForKey:#"status"???
}
}
}];
[request setFailedBlock:^
{
if ([self isViewLoaded])
{
[MBProgressHUD hideHUDForView:self.view animated:YES];
ShowErrorAlert(#"Error", [[request_b error] localizedDescription]);
}
}];
[request startAsynchronous];
}
Or it is better way to change status in my table view if apple notification didn't come or user didn't tap on notification message? Thanks
EDIT:
I don't know how to store data to NSManagedObject *device. Can you help me with this?
I try this, but it didn't works: (on place where you write)
NSInteger statusInt = [status intValue]; //change to int value
NSManagedObject *device = [self.devices objectAtIndex:indexPath.row];
[device setValue:statusInt forKey:#"status"];
EDIT2:
I get it, but problem is with reload table data
NSString *responseString = [request_b responseString];
SBJsonParser *parser = [[SBJsonParser alloc] init];
NSDictionary *result = [parser objectWithString:responseString error:nil];
NSString *status = [result objectForKey:#"status"];
NSInteger statusInt = [status intValue]; //change to int value
NSManagedObject *device = [self.devices objectAtIndex:indexPath.row];
[device setValue:statusInt forKey:#"status"]; //there is problem in save statusInt
// [device setValue:#5 forKey:#"status"]; //if I do this it is ok status is integer16 type
and second problem is in that reload table data. I put there this
[self.tableView reloadData]
but It reloading again and again in loop, what is wrong? I thing there is infinite loop, if I didn't reload table data changes will be visible in next app load. I think problem is that I call
- (void) getStatus:(NSString *)serialNumber atIndexPath:(NSIndexPath *)indexPath
{}
in
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
}
Better should be in viewDidLoad or viewDidApper, but I don't know how make loop for all devices and call
[self getStatus:[device valueForKey:#"serialNumber"] atIndexPath:indexPath];
on that place.
EDIT3:
what if I do it like this:
- (void)viewDidLoad
{
[self updateData];
[self.tableView reloadData];
}
-(void)updateData
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Device"];
request.returnsDistinctResults = YES;
//request.resultType = NSDictionaryResultType;
request.propertiesToFetch = #[#"serialNumber"];
NSArray *fetchedObjects = [self.managedObjectContext
executeFetchRequest:request error:nil];
NSArray *result = [fetchedObjects valueForKeyPath:#"serialNumber"];
//there I get all serialNumbers of my devices and than I call method getStatus and get "new" status and than update it in Core Data.
}
Is that good way to solve this problem? I think better will be if I call getStatus method only one times and get array of statuses.
Maybe I can set all serialNubers in one variable ('xxx','yyyy','zzz') and on server do SELECT * FROM Devices WHERE serialNumber in (serialNuber).
Do you think this could work? I don't have experience how to take data from array to string like ('array_part1','array_part2'....)
Where in your code do you call [UITableView reloadData]?
You should call reloadData on your tableview once you have retrieved the new data from the server. As your server call is async the server call will run on a separate thread while the main thread continues, therefore I presume you have the following problem...
- (void) ...
{
[self getStatus:#"SERIAL_NUMBER"];
[self reloadData]; // This will be called before the async server call above has finished
}
Therefore you are reloading the original data and therefore the new data, which may have loaded a few seconds after, wont be shown.
To fix this, adjust the [getStatus:] method to call the [UITableView reloadData] method on server response.
[request setCompletionBlock:^
{
if([self isViewLoaded])
{
[MBProgressHUD hideHUDForView:self.view animated:YES];
if([request_b responseStatusCode] != 200)
{
ShowErrorAlert(#"Comunication error", #"There was an error communicating with the server");
}
else
{
NSString *responseString = [request_b responseString];
SBJsonParser *parser = [[SBJsonParser alloc] init];
NSDictionary *result = [parser objectWithString:responseString error:nil];
status = [result objectForKey:#"status"];
NSInteger statusInt = [status intValue]; //change to int value
// Store the server response in NSManagedObject *device,
// which will be used as the data source in the tableView:cellForRowAtIndexPath: method
// Once stored, check the tableview isn't NULL and therefore can be accessed
// As this call is async the tableview may have been removed and therefore
// a call to it will crash
if(tableView != NULL)
{
[tableView reloadData];
}
}
}
}];
ASIHTTPRequest is also no longer supported by the developers, I suggest you look into AFNetworking.
Update
In response to the problem you are now having with setting the statusInt within the device NSManagedObject
NSManagedObject *device = [self.devices objectAtIndex:indexPath.row];
[device setValue:statusInt forKey:#"status"]; //there is problem in save statusInt
This is caused as statusInt is an NSInteger which is a primary datatype and not an NSObject as expected by [NSManagedObject setValue:forKey:]. From the documentation for [NSManagedObject setValue:forKey:], the methods expected parameters are as follows.
- (void)setValue:(id)value forKey:(NSString *)key
Therefore you need to pass, in this case, an NSNumber. The problem with NSInteger is that it's simply a dynamic typedef for the largest int datatype based on the current system. From NSInteger's implementation you can see the abstraction.
#if __LP64__
typedef long NSInteger;
#else
typedef int NSInteger;
#endif
If your current system is 64-bit it will use the larger long datatype.
Now, technically the returned status value from the server can be stored as it is without any conversion as an NSString. When you need to retrieve and use the primary datatype of int you can use the [NSString intValue] method you have already used.
Although it's best practice to use a NSNumberFormatter which can be useful for locale based number adjustments and ensuring no invalid characters are present.
NSString *status = [result objectForKey:#"status"];
NSNumberFormatter * f = [[NSNumberFormatter alloc] init];
NSNumber * statusNumber = [f numberFromString:status];
NSManagedObject *device = [self.devices objectAtIndex:indexPath.row];
[device setValue:statusNumber forKey:#"status"];
To retrieve the primary datatype when you wish to use the int within your code, simply call the [NSNumber intValue].
NSNumber *statusNumber = [device objectForKey:#"status"];
int statusInt = [statusNumber intValue];
As for the problem you are having with the infinite loop, this is caused by called [... getStatus:atIndexPath:], which contains the method call reloadData, from within [UITableView tableView:cellForRowAtIndexPath:].
This is because reloadData actually calls [UITableView tableView:cellForRowAtIndexPath:].
Therefore your code continuously goes as the following...
Initial UITableView data load -> tableView:cellForRowAtIndexPath: -> getStatus:atIndexPath: -> Server Response -> reloadData -> tableView:cellForRowAtIndexPath: -> getStatus:atIndexPath: -> Server Response -> reloadData -> ...
Unfortunately you cant just force one cell to update, you have to request the UITableView to reload all data using reloadData. Therefore, if possible, you need to adjust your server to return an unique ID for devices so you can adjust only the updated device within your NSManagedObject.
A suggested alteration for the getStatus method could be just to use the serialNumber if this is stored within the NSManagedObject as a key.
- (void) getStatus:(NSString*)serialNumber
The data source for my UITableview cells is in:
- (void)connectionDidFinishLoading:(NSURLConnection *)conn {
// We are just checking to make sure we are getting the XML
NSString *xmlCheck = [[[NSString alloc] initWithData:xmlData encoding:NSWindowsCP1252StringEncoding] autorelease];
// NSLog(#"xmlCheck2 = %#", xmlCheck);
TFHpple *xpathParser = [[TFHpple alloc] initWithHTMLData:xmlData];
for (int i=2; i<33; i++) {
NSString *link=[NSString stringWithFormat:#"/html/body/table/tr[%d]/td",i];
NSArray *elements = [xpathParser searchWithXPathQuery:link];
NSString *date = [NSString stringWithFormat:#"%#, %#",[elements[5]text],[elements[1]text]];
[times addObject:date];
[names addObject:[elements[2]text]];
[types addObject:[elements[3]text]];
[places addObject:[elements[4]text]];
NSLog(#"%#", elements);
NSLog(#"%#", [elements[0] text]);
}
}
But the method that draws the cell is called before the connection is finished even though the connection is started before I draw the cells. How do I delay the draw cell method or make sure the connection is finished before I draw the cells?
You need to set tableview's datasource & delegate properties only after you finish loading data.
If you have set delegate & datasource of tableview from IB or Storyboard, remove it. Set delegate & datasource properties of tableview after you finish loading data. And reload table as well.
- (void)connectionDidFinishLoading:(NSURLConnection *)conn {
// We are just checking to make sure we are getting the XML
NSString *xmlCheck = [[[NSString alloc] initWithData:xmlData encoding:NSWindowsCP1252StringEncoding] autorelease];
// NSLog(#"xmlCheck2 = %#", xmlCheck);
TFHpple *xpathParser = [[TFHpple alloc] initWithHTMLData:xmlData];
for (int i=2; i<33; i++) {
NSString *link=[NSString stringWithFormat:#"/html/body/table/tr[%d]/td",i];
NSArray *elements = [xpathParser searchWithXPathQuery:link];
NSString *date = [NSString stringWithFormat:#"%#, %#",[elements[5]text],[elements[1]text]];
[times addObject:date];
[names addObject:[elements[2]text]];
[types addObject:[elements[3]text]];
[places addObject:[elements[4]text]];
NSLog(#"%#", elements);
NSLog(#"%#", [elements[0] text]);
}
[tableView setDelegate:self];// set delegate, datasource & reload data.
[tableView setDatasource:self];
[tableView reloadData];
}
that's the point of ASYNCHRONOUS networking :) Your main thread doesnt wait for it to finish! synchronous is there but it's bad
Have your UI handle the case that data isnt available yet.
a) set tableview hidden, show a spinning wheel and show and reload the table when connectionDidFinish is called
mock code
-viewWillAppear {
table.hidden = YES;
spinningActivity.hidden = NO;
networkConnection start];
}
-connectionDidFinish {
spinningActivity.hidden = YES;
[table reloadData];
table.hidden = NO;
}
The method that provides cell information is only called if you tell it there are rows ready to display. If you tell it the right thing in tableView:numberOfRowsInSection: -- which could be 0 if the connection hasn't finished -- there shouldn't be any incorrect calls to tableView:cellForRowAtIndexPath:.