I'm making chat application and read messages data from SQLite database. I want to reload my TableView data with new messages after clicking on my "send" button.
Event for my button:
[_sendButton addTarget:self action:#selector(saveMessage:) forControlEvents:UIControlEventTouchUpInside];
I think I need to use -(void)viewDidAppear:(BOOL)animated but it didn't work (or I'm doing something wrong).
[_tableView reloadData];
-(void)viewDidAppear:(BOOL)animated {
sqlite3 *database;
databaseName = #"Messenges.db";
NSArray *documentPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDir = [documentPaths objectAtIndex:0];
databasePath = [documentsDir stringByAppendingPathComponent:databaseName];
messenges = [[NSMutableArray alloc] init];
if(sqlite3_open([databasePath UTF8String], &database) == SQLITE_OK) {
const char *sqlStatement = "select * from messenges";
sqlite3_stmt *compiledStatement;
if(sqlite3_prepare_v2(database, sqlStatement, -1, &compiledStatement, NULL) == SQLITE_OK) {
while(sqlite3_step(compiledStatement) == SQLITE_ROW) {
NSString *dbMessageText = [NSString stringWithUTF8String:(char *)sqlite3_column_text(compiledStatement, 1)];
Message *messege = [[Message alloc] initWithName:dbMessageText];
[messenges addObject:messege];
[messege release];
}
}
sqlite3_finalize(compiledStatement);
}
sqlite3_close(database);}
Editing
This is my method to add a new row to TableView. New row must be added at once after clicking "send" button.
-(IBAction)insertRows:(id)sender {
MDAppDelegate *appDelegate = (MDAppDelegate *)[[UIApplication sharedApplication] delegate];
[messenges insertObject:[[NSString alloc] initWithFormat:#"%#", _textField.text] atIndex:appDelegate.messenges.count+1];
NSArray *insertIndexPaths = [NSArray arrayWithObject: [NSIndexPath indexPathForRow:1 inSection:1]];
UITableView *tV = (UITableView *)self.tableView;
[tV beginUpdates];
[tV insertRowsAtIndexPaths:insertIndexPaths withRowAnimation:UITableViewRowAnimationRight];
[tV endUpdates];}
But this code give me error: 'Invalid table view update. The application has requested an update to the table view that is inconsistent with the state provided by the data source.'
How can I fix it?
Edit2
Ok. I have only 1 section. numberOfSectionsInTableView: does not require correction.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
MDAppDelegate *appDelegate = (MDAppDelegate *)[[UIApplication sharedApplication] delegate];
NSUInteger numberOfRowsInSection = appDelegate.messenges.count; //add new variable to what rows will be +1
if (self.editing) {
numberOfRowsInSection++;
}
return numberOfRowsInSection;}
But I still have the same error.
Edit #3
Let's look. I changed my insertRows to :
-(IBAction)insertRows:(id)sender {
MDAppDelegate *appDelegate = (MDAppDelegate *)[[UIApplication sharedApplication] delegate];
[self.tableView beginUpdates];
self.tableView.editing = YES;
[messenges insertObject:[[NSString alloc] initWithFormat:#"%#", _textField.text] atIndex:appDelegate.messenges.count+1];
NSArray *insertIndexPaths = [NSArray arrayWithObject: [NSIndexPath indexPathForRow:appDelegate.messenges.count+1 inSection:1]];
[self.tableView insertRowsAtIndexPaths:insertIndexPaths withRowAnimation:UITableViewRowAnimationRight];
[self.tableView endUpdates];
}
And numberOfRowsInSection:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
MDAppDelegate *appDelegate = (MDAppDelegate *)[[UIApplication sharedApplication] delegate];
NSUInteger numberOfRowsInSection = appDelegate.messenges.count;
if (self.tableView.editing) {
numberOfRowsInSection++;
}
NSLog(#"%i",numberOfRowsInSection);
return numberOfRowsInSection;
}
But I got an error *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 4 beyond bounds [0 .. 3]' . Then I corrected if (self.tableView.editing) to:
if (self.tableView.editing) {
MDAppDelegate *someNewDelegate = (MDAppDelegate *)[[UIApplication sharedApplication] delegate];
numberOfRowsInSection = someNewDelegate.messenges.count+1; //or .count
}
But got same error about 'NSRangeException'.
To update your UITableView with animation you need to:
Invoke beginUpdates on your UITableView
Update your data model
Insert / Remove rows and sections
Invoke endUpdates on your UITableView
Couple of Things to Keep in Mind:
If you are deleting a section you should not also be deleting rows from that section (unnecessary and might even cause errors)
If you are inserting a section you should not also be inserting rows into that section (UITableView will call numberOfRowsInSection: followed by cellForRowAtIndexPath: if the cell is visible).
What Do You Mean, Update My Data Model?
If you are manipulating a UITableView by adding and removing rows and sections, then you should have some way of keeping track of what sections / rows are currently in the UITableView at any given moment.
For example, you should have a numberOfRowsInSection: method that resembles this:
- (NSInteger)numberOfRowsInSection:(NSInteger)section
{
// You should have a way to get section data with an index (section)
// The below example assumes an NSArray named self.mySectionCollection
// exists for this purpose
NSArray *sectionList = self.mySectionCollection;
// You should have data associated with a section. Below its assumed
// this data is wrapped in an NSDictionary. Data that might be in here
// could include a height, a name, number of rows, etc...
NSDictionary *sectionData = [sectionList objectAtIndex:section];
NSInteger rowCount = 0;
// SectionData should NEVER be nil at this point. You could raise an
// exception here, have a debug assert, or just return 0.
if (sectionData)
{
rowCount = [[sectionData objectForKey:#"rowCount"] intValue];
}
return rowCount;
}
You can store information about sections / rows in several different ways including using CoreData. The point is that by modifying this collection in between beginUpdates and endUpdates you are telling the UITableView how to update itself (it will query numberOfRowsInSection:, numberOfSections, and cellForRowAtIndexPath: as needed).
Edit
I believe if you modify your insertRows: method to modify your data source (messenges) at the same time you notify UITableView that updates have occurred that things will begin working for you properly.
Try using this code:
-(IBAction)insertRows:(id)sender
{
MDAppDelegate *appDelegate = (MDAppDelegate *)[[UIApplication sharedApplication] delegate];
UITableView *tV = (UITableView *)self.tableView;
[tV beginUpdates];
// UPDATE DATA MODEL IN BETWEEN beginUpdates and endUpdates
int rowIndex = appDelegate.messenges.count + 1;
[messenges insertObject:[[NSString alloc] initWithFormat:#"%#", _textField.text]
atIndex:rowIndex];
// Notify UITableView that updates have occurred
NSArray *insertIndexPaths = [NSArray arrayWithObject:
[NSIndexPath indexPathForRow:rowIndex inSection:1]];
[tV insertRowsAtIndexPaths:insertIndexPaths
withRowAnimation:UITableViewRowAnimationRight];
[tV endUpdates];
}
Edit #2
If you are still having issues, I would look at where you are setting the self.editing flag.
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
MDAppDelegate *appDelegate = (MDAppDelegate *)[[UIApplication sharedApplication] delegate];
//add new variable to what rows will be +1
NSUInteger numberOfRowsInSection = appDelegate.messenges.count;
if (self.editing)
{
numberOfRowsInSection++;
}
return numberOfRowsInSection;
}
This flag controls whether an additional row exists in the table or not. For this reason, you must set it between beginUpdates and endUpdates like:
// assuming the editing flag is set from some IBAction
-addNewRow:(id)sender
{
int row = ???; // not sure where you want this new row
[self.tableView beginUpdates]
self.editing = YES;
NSArray *insertIndexPaths = [NSArray arrayWithObject:
[NSIndexPath indexPathForRow:row inSection:1]];
[self.tableView insertRowsAtIndexPaths:insertIndexPaths
withRowAnimation:UITableViewRowAnimationRight];
[self.tableView endUpdates];
}
Remember to similarly call deleteRowsAtIndexPaths:withRowAnimation: if the user stops editing if you are removing the row you add. I believe no action is necessary if the edit becomes permanent, but you'd need to set self.editing = NO while also adding a new row to it's proper place in self.messenges.
Also, in your insertRows: method, you telling the UITableView that you are inserting a row at index 1 when in fact you always insert at the end of the messenges collection. I've modified my version of the insertRows method so that it has a rowIndex variable for the purpose of ensuring the same index is used when updating the data model and informing UITableView of the change.
Lastly, please include as much debugging information as possible when you run into problems. Usually when a problem arises from updating the UITableView in the manner you are trying, it will tell you there is an inconsistency error. It's been awhile since I've seen it, but something to the effect that before updating the table there were 5 number of rows and after there were 7 when only 1 was added. I'd like to see this message if it is showing up in your console.
Edit #3
This is in response to the error you are seeing:
* Terminating app due to uncaught exception 'NSRangeException', reason: '* -[__NSArrayM objectAtIndex:]: index 4 beyond bounds [0 ..
3]'
Your error has nothing to do with inserting into the UITableView. It has to do with the fact that you are trying to insert beyond the bounds of an array. My guess is the offending line is:
[messenges insertObject:[[NSString alloc] initWithFormat:#"%#",
_textField.text]
atIndex:appDelegate.messenges.count+1];
To insert at the end of the array, use an index of appDelegate.messenges.count (removed the +1).
Also... Are you absolutely certain your data model and calls to update the UITableView agree? Try invoking NSLog(#"rowsBeforeUpdate: %i", [self numberOfRowsInSection:0]); just after you call beginUpdates and just before calling endUpdates. Also, call NSLog(#"inserting index paths: %#", insertIndexPaths) when informing the UITableView of an insert operation. My guess is that self.messenges accurately reflects the rows that should be in the table, but by adding +1 when self.tableView.editing is true, you push the numberOfRowsInSection: above what you report in your calls to insertRowsAtIndexPaths:withRowAnimation:.
Try this:
-(IBAction)insertRows:(id)sender {
MDAppDelegate *appDelegate = (MDAppDelegate *)[[UIApplication sharedApplication] delegate];
[self.tableView beginUpdates];
NSLog(#"rows before update: %i", [self numberOfRowsInSection:0]);
self.tableView.editing = YES;
NSLog(#"rows with editing flag set: %i", [self numberOfRowsInSection:0]);
int rowIndex = appDelegate.messenges.count; // don't need +1 at end
int sectionIndex = 0; // should it be 0 or 1? I would guess 0
[messenges insertObject:[[NSString alloc] initWithFormat:#"%#", _textField.text]
atIndex:rowIndex];
NSArray *insertIndexPaths = [NSArray arrayWithObject:
[NSIndexPath indexPathForRow:rowIndex
inSection:sectionIndex]];
[self.tableView insertRowsAtIndexPaths:insertIndexPaths
withRowAnimation:UITableViewRowAnimationRight];
NSLog(#"inserting index paths: %#", insertIndexPaths);
[self.tableView endUpdates];
}
I changed the row index above to exclude the +1 in it. I changed the section index to be 0, which should be correct unless this UITableView does in fact have multiple sections that haven't been mentioned. Make sure that your logs make sense. If you see numberOfRowsInSection: increase by two during the course of insertRows: then you had also better see insertRowsAtIndexPaths: reflect the same thing. I'm confused why you need the editing flag to add another row in your numberOfRowsInSection: method. This part (where you add an additional row if self.editing is set) doesn't seem to be working right. Perhaps just try leaving out this part to have some of it working, then add it back in once you have some things working properly. Perhaps change numberOfRowsInSection: to be like the following until some things begin working:
- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
MDAppDelegate *appDelegate = (MDAppDelegate *)[[UIApplication sharedApplication] delegate];
NSUInteger numberOfRowsInSection = appDelegate.messenges.count;
NSLog(#"numberOfRowsInSection %i: %u", section, numberOfRowsInSection);
return numberOfRowsInSection;
}
Related
I'm experiencing an issue with my UITableView which fetches data from a data table on parse.com. The issue is that every time I scroll down, hiding the first cell completely and then scroll back up, the text on the first cell's titleL is that of another cell. Kindly look at my code and let me know what I'm doing wrong. Also are there any better practices for my code when working with UITableViews in the future?
Code
- (void)viewDidLoad {
[super viewDidLoad];
[self someMethod];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *CellIdentifer = [NSString stringWithFormat:#"CellIdentifier%i",num];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifer];
if (cell == nil) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifer];
}
UILabel *titleL = [[UILabel alloc] initWithFrame:CGRectMake(10,10,300,20)];
titleL.text = myTitle;
[cell addSubview:titleL];
return cell;
}
-(void) someMethod {
for (int i = 0; i < arr.count; i++) {
PFQuery *query = [PFQuery queryWithClassName:#"SomeClass"];
[query whereKey:#"objectId" equalTo:[arr objectAtIndex:i]];
[query getFirstObjectInBackgroundWithBlock:^(PFObject *object, NSError *error) {
if (!object) {
} else {
myTitle = [object objectForKey:#"title"];
num = i;
[feed beginUpdates];
[feed reloadRowsAtIndexPaths:myArr withRowAnimation:UITableViewRowAnimationAutomatic];
[feed endUpdates];
}
}];
}
}
You need to write your your tableView:cellForRowAtIndexPath: in such a way that it doesn't matter in which order it is called.
That method is called whenever the UITableView needs to get a cell (sometimes this doesn't mean that it's displayed). It will get called multiple times and you cannot rely on a specific order (for obvious reasons: you cannot predict how the user will scroll).
Now, your problem is that your implementation uses myTitle to assign a title. But that value is not calculated inside tableView:cellForRowAtIndexPath:. You need to change your code in such a way that you always can access the required value for your index path, no matter in which order or how often that method is called.
For example, in someMethod you can store your values from [object objectForKey:#"title"] in an NSMutableArray or in a NSMutableDictionary (with #(i) as key). Then you can query the title for each index path in tableView:cellForRowAtIndexPath:.
I am using UITableViewController and getting this error while updating tableView. Below is my code:
This occurs when i do a click event:
[timeZoneNames insertObject:#"HELLO" atIndex:0];
[self.tableView beginUpdates];
NSArray *insertIndexPaths = [NSArray arrayWithObject:[NSIndexPath indexPathForRow:0 inSection:0]];
[self.tableView insertRowsAtIndexPaths:insertIndexPaths withRowAnimation:UITableViewRowAnimationTop];
[self.tableView endUpdates];
I tried looking for apple documentation but that didnt helped.
Thanks,
Aby
I ran into this error when I forgot to set the datasource outlet properly.
If you see this error, check that you have explicitly set your TableView delegate and datasource :
go to your interface builder and look at the view with the 'Table View'
cmd + right click drag ( you should see a blue line ) from the 'Table View' icon to the title icon for the file, this has a yellow icon next to it in xcode 6.
release your mouse , you should see delegate and datasource options.
select 'datasource'
your table should now be correctly wired.
I've ran into this problem before. It means that when -insertRowsAtIndexPaths:withRowAnimation: was called -tableView:numberOfRowsInSection: returned 0. You need to insert into the model before you call -insertRowsAtIndexPaths:withRowAnimation: (see: -beginUpdates)
Update
I wonder what the return value of -tableView:numberOfRowsInSection: is? Also, you don't need -beginUpdates/-endUpdates if you only have one update.
[timeZoneNames insertObject:#"HELLO" atIndex:0];
// Let's see what the tableView claims is the the number of rows.
NSLog(#"numberOfRowsInSection: %d", [self tableView:self.tableView numberOfRowsInSection:0]);
NSArray *insertIndexPaths = [NSArray arrayWithObject:[NSIndexPath indexPathForRow:0 inSection:0]];
[self.tableView insertRowsAtIndexPaths:insertIndexPaths withRowAnimation:UITableViewRowAnimationTop];
I tried printing out how many rows and sections were actually in the tableView during the update which got me thinking, how many sections am I returning in the table view data source method...and this was the culprit:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 0;
}
Make sure you are returning 1 and not 0.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
At least this solved the problem for me...
UPDATE:
I'm having the same problem again after it was working for a while. I had modified the app a bit. My first view is a tableView and when you click the plus button on the upper right, you get another table where you can input information. When you click done, the information is added to the Core Data model with this code:
- (IBAction)doneButtonPressed:(UIBarButtonItem *)sender
{
MoneyBadgeAppDelegate *appDelegate =
[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context =
[appDelegate managedObjectContext];
NSManagedObject *newMoment;
newMoment = [NSEntityDescription
insertNewObjectForEntityForName:#"SpendingMoment"
inManagedObjectContext:context];
NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
[dateFormat setDateFormat:#"yyyyMMdd"];
NSDate *modifiedDate = [dateFormat dateFromString: self.dateTextField.text];
[newMoment setValue: modifiedDate forKey:#"date"];
[newMoment setValue: descTextField.text forKey:#"desc"];
[appDelegate.eventsArray insertObject:newMoment atIndex:0];
NSError *error;
[context save:&error];
[self dismissViewControllerAnimated:YES completion:nil];
}
And I confirmed it gets put into the Core Data model successfully by printing to the console the following in my viewDidAppear method upon returning back to the first screen.
-(void)viewDidAppear:(BOOL)animated{
NSEntityDescription *entity = [NSEntityDescription entityForName:#"SpendingMoment"
inManagedObjectContext:self.managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:entity];
NSError *error;
NSArray *items = [self.managedObjectContext
executeFetchRequest:fetchRequest error:&error];
for (SpendingMoment *moment in items) {
NSLog(#"Date: %#, Desc: %#", [moment date], [moment desc]);
}
[self addEvent];
And then my addEvent method does this:
- (void)addEvent {
[self.tableScreen beginUpdates];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableScreen insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
[self.tableScreen reloadData];
[self.tableScreen endUpdates];
#try{
[self.tableScreen scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:YES];
}
#catch(NSException *exception){
}
}
Any idea what the problem is this time??? Thanks!
Check this "attempt to insert row 0 into section 0, but there are only 0 rows in section 0 after the update".
It mesns when you insert object in row 0 , section 0, there are no any sections.
Try to insert section before you insert row.
[timeZoneNames insertObject:#"HELLO" atIndex:0];
NSLog(#"sections: %lu ",[self.downlaodTableView numberOfSections];
// insert section
[self.tableView insertSections:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, 1)] withRowAnimation:UITableViewRowAnimationNone];
NSLog(#"numberOfRowsInSection: %d", [self tableView:self.tableView numberOfRowsInSection:0]);
NSArray *insertIndexPaths = [NSArray arrayWithObject:[NSIndexPath indexPathForRow:0 inSection:0]];
[self.tableView insertRowsAtIndexPaths:insertIndexPaths withRowAnimation:UITableViewRowAnimationTop];
I have a tableview which is sectioned alphabetically. I have an NSMutableArray *drinks which is my original datasource which its being populated with NSMutableDictionary's from a plist. in my viewDidLoad method. I then iterate through my array and create the keys for the sections and the number of rows in each section in my viewWillAppear method
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.sections = [[NSMutableDictionary alloc] init];
BOOL found;
// Loop through the whiskys and create our keys
for (NSMutableDictionary *whisky in self.drinks)
{
NSString *c = [[whisky objectForKey:NAME_KEY] substringToIndex:1];
found = NO;
for (NSString *str in [self.sections allKeys])
{
if ([str isEqualToString:c])
{
found = YES;
}
}
if (!found)
{
[self.sections setValue:[[NSMutableArray alloc] init] forKey:c];
}
}
// Loop again and sort the whiskys into their respective keys
for (NSDictionary *whisky in self.drinks)
{
[[self.sections objectForKey:[[whisky objectForKey:NAME_KEY] substringToIndex:1]] addObject:whisky];
}
// Sort each section array
for (NSString *key in [self.sections allKeys])
{
[[self.sections objectForKey:key] sortUsingDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:NAME_KEY ascending:YES]]];
}
[self.tableView reloadData];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view
NSArray *searchPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [searchPaths lastObject];
NSString *writeableDBPath = [documentsDirectory stringByAppendingPathComponent:#"ScotchList.plist"];
NSMutableArray *tmpArray = [[NSMutableArray alloc]initWithContentsOfFile:writeableDBPath];
self.drinks = tmpArray;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(applicationDidEnterBackground:)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
//Register for application exiting information so we can save data
[[NSNotificationCenter defaultCenter]addObserver:self selector:#selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil];
}
All of this works fine and dandy. Then when I delete a row it works fine as well. How ever when I go to a new view it and come back to the tableview the deleted row reappears. So I need to update my original datasource (drinks) because everytime the viewAppears it pulls from the original datasource again. So I tried to get the item (whisky) at the row and remove it from the array. But it throws a sigabrt. This is my delete row method.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
//DELETE THE ROW FROM THE DATA SOURCE
[self.tableView beginUpdates];
[[self.sections valueForKey:[[[self.sections allKeys] sortedArrayUsingSelector:#selector(localizedCaseInsensitiveCompare:)] objectAtIndex:indexPath.section]] removeObjectAtIndex:indexPath.row];
NSDictionary *whisky = [[self.sections valueForKey:[[[self.sections allKeys] sortedArrayUsingSelector:#selector(localizedCaseInsensitiveCompare:)] objectAtIndex:indexPath.section]] objectAtIndex:indexPath.row];
[self.drinks removeObject:whisky];
if ([[self.sections valueForKey:[[[self.sections allKeys] sortedArrayUsingSelector:#selector(localizedCaseInsensitiveCompare:)] objectAtIndex:indexPath.section]] count] > 0)
{
// Section is not yet empty, so delete only the current row.
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
else
{
// Section is now completely empty, so delete the entire section.
[self.sections removeObjectForKey:[[[self.sections allKeys] sortedArrayUsingSelector:#selector(localizedCaseInsensitiveCompare:)] objectAtIndex:indexPath.section]];
[tableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section]
withRowAnimation:UITableViewRowAnimationFade];
}
[self.tableView endUpdates];
} else if (editingStyle == UITableViewCellEditingStyleInsert) {
// Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view.
}
}
this is what the log shows:
2012-07-06 15:36:42.454 WhiskyBook[3275:907] *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndex:]: index 1 beyond bounds [0 .. 0]'
*** First throw call stack:
(0x3998a17b 0x3494395b 0x398d83bd 0x3e6b5 0x32a0cd11 0x328f0d09 0x328f0cbb 0x328f0c95 0x328f09eb 0x328f1369 0x328ef791 0x328dd72d 0x328dd009 0x339a7603 0x339a7233 0x3995a873 0x3995a817 0x39959611 0x398d6be5 0x398d6a71 0x339a634b 0x329037f5 0x3ac09 0x33cc7b20)
libc++abi.dylib: terminate called throwing an exception
(lldb)
What am I doing wrong?
I found a solution to my problem. Instead of trying to remove the object from the original datasource inside of the commitEditingStyle method I just made an NSDictionary property for my master-viewController alloc'd and inited it in my viewDidLoad then assigned it the item that I want to be deleted inside of the commitEditingStyle method. Then inside of my viewWillDisappear method I removed it from the original datasource.
I've got a fairly important conceptual issue that many people have asked about, but there isn't a readily available clear answer to be found by searching.
My application is simple: Several rows of TableViewCells populated with data from a parsed JSON feed. When a cell is clicked on, that cell's info is passed to a SecondViewController and displayed. The JSON feed is also stored to a .plist and in the case that the internet is not available, the TableViewCells are populated from the .plist.
This is all working great.
However, the last thing I need is a refresh button at the top of my FirstViewController to refresh the JSON feed, and all of the cells in the table with the new data from the new variables. However, I've encountered an issue with implementing this:
My original JSON call, and variables to populate the cells are located in the ViewDidLoad method. When the view loads, these variables are "set" and don't refresh. Further, I can move the JSON call and variables into viewWillLoad - which will refresh the table each time after clicking on a cell, and then clicking "back" to the firstViewController -- this will update the JSON and cells successfully, however it does impact the speed and makes the view controller "pause" when going back to the MainViewController, which makes calling my original JSON and setting my variables in viewWillLoad an unviable option.
I have created a reload button in ViewDidLoad, which is linked to an IBAction method "refresh":
Create Button Programitically in ViewDidLoad:
// Reload issues button
UIBarButtonItem *button = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh
target:self
action:#selector(refresh:)];
self.navigationItem.rightBarButtonItem = button;
[button release];
Action Method it's linked to:
- (IBAction)refresh:(id)sender {
myRawJson = [[NSString alloc] initWithContentsOfURL:[NSURL
URLWithString:#"http://www.yoursite.com/json.JSON"]
encoding:NSUTF8StringEncoding
error:nil];
SBJsonParser *parser = [[SBJsonParser alloc] init];
NSDictionary * myParsedJson = [parser objectWithString:myRawJson error:NULL];
// New updated dictionary built from refreshed JSON
allLetterContents = [myParsedJson objectForKey:#"nodes"];
// Log the new refreshed JSON
NSLog(#"You clicked refresh. Your new JSON is %#", myRawJson);
//Maybe use the notification center?? But don't know how to implement.
//[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(refreshView:)
name:#"refreshView" object:nil];
//[[NSNotificationCenter defaultCenter] postNotificationName:#"refreshView"
object:nil];
}
[self.tableView reloadRowsAtIndexPaths:[self.tableView indexPathsForVisibleRows]
withRowAnimation:UITableViewRowAnimationNone];
[myRawJson release];
}
In the code above you can see that I'm re-calling the JSON each time the button is clicked and logging a message to console with the new JSON. This is working. I've even re-built a dictionary which is successfully adding the new content.
My question is: How can I make the tableViewCells "refresh" with this new data as well? Can I just make the button re-load the entire view controller - so it would call ViewDidLoad again? Do I need to re-think my apps structure, or move my original variables out of viewDidLoad?
I've been reading some posts on the NSNotificationCenter, but the implementation of this still baffles me, as I'm fairly new to iOS development.
Thanks~
Update:
It's still not updating. Here is my full refresh button code with [self.tableView reloadData]; called at the end of my IBAction.
- (IBAction)refresh:(id)sender {
[DSBezelActivityView newActivityViewForView:
self.navigationController.navigationBar.superview
withLabel:#"Loading Feed..." width:160];
myRawJson = [[NSString alloc] initWithContentsOfURL:[NSURL
URLWithString:#"http://site.com/mobile.JSON"]
encoding:NSUTF8StringEncoding
error:nil];
SBJsonParser *parser = [[SBJsonParser alloc] init];
NSDictionary * myParsedJson = [parser objectWithString:myRawJson error:NULL];
allLetterContents = [myParsedJson objectForKey:#"nodes"];
BOOL isEmpty = ([myParsedJson count] == 0);
if (isEmpty) {
NSString *refreshErrorMessage = [NSString
stringWithFormat:#"An internet or network connection is required."];
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle:#"Alert"
message: refreshErrorMessage
delegate:self
cancelButtonTitle:#"Close"
otherButtonTitles:nil];
[alert show];
[alert release];
allLetterContents = [NSMutableDictionary
dictionaryWithContentsOfFile:[self saveFilePath]];
//NSLog(#"allLetterContents from file: %#", allLetterContents);
} else {
NSLog(#"Your new allLetterContents is %#", allLetterContents);
// Fast enumeration through the allLetterContents NSMutableDictionary
for (NSMutableDictionary * key in allLetterContents) {
NSDictionary *node = [key objectForKey:#"node"];
NSMutableString *contentTitle = [node objectForKey:#"title"];
NSMutableString *contentNid = [node objectForKey:#"nid"];
NSMutableString *contentBody = [node objectForKey:#"body"];
// Add each Title and Nid to specific arrays
//[self.contentTitleArray addObject:contentTitle];
[self.contentTitleArray addObject:[[contentTitle
stringByReplacingOccurrencesOfString:#"&"
withString:#"&"] mutableCopy]];
[self.contentNidArray addObject:contentNid];
[self.contentBodyArray addObject:contentBody];
}
}
[self.tableView reloadData];
[DSBezelActivityView removeViewAnimated:YES];
[myRawJson release];
}
I'm configuring the cell at cellForRowAtIndexPath (Updated: Posted entire method):
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
}
}
// Configure the cell.
cell.textLabel.text = [self.contentTitleArray objectAtIndex: [indexPath row]];
cell.detailTextLabel.text = [self.contentNidArray objectAtIndex: [indexPath row]];
return cell;
}
Setting it on didSelectRowAtIndexPath:
self.detailViewController.currentNodeTitle = [contentTitleArray
objectAtIndex:indexPath.row];
self.detailViewController.currentNodeNid= [contentNidArray
objectAtIndex:indexPath.row];
self.detailViewController.currentNodeBody = [contentBodyArray
objectAtIndex:indexPath.row];
So when clicking my refresh button the table should* refresh with the new json, but does not.. Am I missing a step?
Additionally this may not be important, but I'm changing the colors for every other row with:
// Customize the appearance of table view cells.
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row % 2)
{
[cell setBackgroundColor:[UIColor colorWithRed:221.0/255.0 green:238.0/255.0 blue:255.0/255.0 alpha:1]];
cell.textLabel.textColor = [UIColor colorWithRed:2.0/255.0 green:41.0/255.0 blue:117.0/255.0 alpha:1];
cell.detailTextLabel.textColor = [UIColor colorWithRed:2.0/255.0 green:41.0/255.0 blue:117.0/255.0 alpha:1];
} else [cell setBackgroundColor:[UIColor clearColor]];
}
Update
You need to call the reload method.
[self.tableView reloadData];
This will fire the dataSource and delegate events an will refresh the UITableView.
You can find more info in the UITableView Class Reference:
Call this method to reload all the data that is used to construct the table, including cells, section headers and footers, index arrays, and so on. For efficiency, the table view redisplays only those rows that are visible.
In my application I have the array, which create in Main_View_Controller from json response in loop:
Main_View_Controller.m
NSMutableArray *Cities = [[NSMutableArray alloc] init];
while (ItemsFromParsedResponse = (NSDictionary *)[enumerator nextObject]) {
AppDelegate *dataCenter = (AppDelegate*)[[UIApplication sharedApplication] delegate];
dataCenter.CityLabel = [ItemsFromParsedResponse objectForKey:#"label"];
[Cities addObject:dataCenter.CityLabel];
dataCenter = nil;
}
AppDelegate *dataCenter = (AppDelegate*)[[UIApplication sharedApplication] delegate];
dataCenter.CityInfo = Cities;
This array must be presented in Popover, containing TableView. I've trying to delegate array to popover same like delegate it to AppDelegate, but it's not work. If I read dataCenter.CityInfo in my CityList_Popover_Contoller, it has nil value.
CityList_Popover_Controller.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{//some standard code
AppDelegate *dataCenter = (AppDelegate*)[[UIApplication sharedApplication] delegate];
cell.textLabel.text = [dataCenter.CityInfo objectAtIndex:indexPath.row];
[tableView reloadData];
return cell;
}
How can I load CityInfo only if it is not-nil? And how can I tracking changes in this array and dynamically update table content according new data in array?
Sorry if my question too simple, but I spent a lot of time to make it work.
Thanks for any advice!
Just a simple if-check should work:
AppDelegate *dataCenter = appDelegate;
if ([dataCenter.CityInfo objectAtIndex:indexPath.row])
{
cell.textLabel.text = [dataCenter.CityInfo objectAtIndex:indexPath.row];
}
else
{
NSLog(#"Whoops, null data at row %i", indexPath.row);
}
// NEVER call reload data here, you are
// already reloading data when this
// method is called, will end up in corruption.
// [tableView reloadData];
return cell;