I'm having a bit of trouble trying to delete rows that haven't been loaded (or not visible) from a UITableview.
My setup is as follows -
There are two sections in the tableview and each element in section 1 is associated with multiple elements from section two.
To give you an example (The data isn't really related to what I'm trying to do, but I believe this will be an example that doesn't really require much explanation)
Section 1
BMW
Acura
Merc
Section 2
328i
335i
RX
LX
TX
C300
C550
My internal model goes something like this -
NSMutableArray Cars[]
NSMutableArray Models[]
cars[0] = "BMW"
cars[1] = "Acura"
cars[2] = "Merc"
Each element in Models[] is a vector and their conents are listed below
Models = [ ["328i", "335i"], ["RX", "LX", "TX"], ["C300", "C550"] ];
So for the functionality I'm trying to build. If the user clicks delete and tries to delete BMW, the App needs to remove the entry for BMW from section 1 and the entries for 328i and 335i in section two. However, the user is free to delete any single row of section two independently.
Can anyone point me to a way I can proceed with this?
NSMutableArray *Cars = [[NSMutableArray alloc] initWithObjects:#"BMW",#"Acura",#"Merc",nil];
NSMutableArray *Arr1 = [[NSMutableArray alloc]initWithObjects:#"328i", #"335i",nil];
NSMutableArray *Arr2 = [[NSMutableArray alloc]initWithObjects:#"RX", #"LX", #"TX",nil];
NSMutableArray *Arr3 = [[NSMutableArray alloc]initWithObjects:#"C300", #"C550",nil];
NSMutableArray * Models = [[NSMutableArray alloc] initWithObjects:Arr1,Arr2,Arr3,nil];
On delete if you delete BMW then remove 1st element from Models array and and 1st element from Cars array and reload table.
i.e.
[Cars removeObjectAtIndex:0];
[Models removeObjectAtIndex:0];
[tableview reload]; //tableview - object of UiTableView
- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
int row = indexPath.row;
if(indexPath.section ==0) {
// Need to remove both the Car and the Models associated with it.
// Begin updates. Doesn't commit edits till endUpdates;
[tableView beginUpdates];
// Get the indexPaths to be removed.
// Cars is straight forward.
NSMutableArray *carsIndexPath = [[NSMutableArray alloc] init];
NSIndexPath *carsIndexPathObj = [NSIndexPath indexPathForRow:indexPath.row inSection:0];
[carsIndexPath addObject:carsIndexPathObj];
// Manually make index paths for models associated with the car.
NSMutableArray *modelIndexPaths = [self getIndexPaths:indexPath.row];
// Now remove from model
[models removeObjectAtIndex:indexPath.row];
[cars removeObjectAtIndex:indexPath.row];
// Remove from Table
[tableView deleteRowsAtIndexPaths:carsIndexPaths withRowAnimation:UITableViewRowAnimationLeft];
[tableView deleteRowsAtIndexPaths:modelIndexPaths withRowAnimation:UITableViewRowAnimationLeft];
// Commit updates
[tableView endUpdates];
// Reload data.
[tableView reloadData];
}
}
-(NSMutableArray *) getIndexPaths:(NSInteger) index {
NSMutableArray *indexPathArray = [[NSMutableArray alloc] init];
int offset = 0;
for (int i=0; i<index; i++) {
NSMutableArray *tempArr = [models objectAtIndex:i];
offset += [tempArr count];
}
NSMutableArray *currentModels = [models objectAtIndex:index];
for (int i=0; i<[currentModels count]; i++) {
NSIndexPath *indexPathTemp = [NSIndexPath indexPathForRow:offset+i inSection:1];
[indexPathArray addObject:indexPathTemp];
}
return indexPathArray;
}
(If this makes any sense at all)
Related
Now I am taking index paths of visible tableview cells.But I want to take 3 more index path from visible rows,How can I do that ?
NSArray *visiblePaths = [tblView indexPathsForVisibleRows];
Assuming just one section, you can just create them, minding the max rows for that section:
NSMutableArray *visiblePaths = [tblView indexPathsForVisibleRows] mutableCopy];
NSInteger lastIndexPath = [visiblePaths lastObject];
NSInteger lastRow = lastIndexPath.row;
NSInteger extraRows = 3;
NSInteger maxRow = MIN(lastRow+extraRows, [self tableView:tblView numberOfRowsInSection:0] - 1);
for (int i = lastRow+1; i < maxRow; i++) {
NSIndexPath *newPath = [NSIndexPath indexPathForRow:i inSection:0];
[visiblePaths addObject:newPath];
}
It's doable for more sections (and/or for rows before the first visible) also. Hopefully it's clear how one would extend this to cover those requirements.
Ok so I have wrote some code in this function:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSLog(#"tableviwcell is getting called");
properties = [NSUserDefaults standardUserDefaults];
NSDictionary *events_dict = [properties objectForKey:#"events_dict"];
NSDictionary *events = [events_dict objectForKey:#"event"];
am_participants = [events objectForKey:#"amParticipants"];
pm_participants = [events objectForKey:#"pmParticipants"];
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:#"currentCell"];
if(cell == nil){
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:#"currentCell"];
}
int sizeofam = [am_participants count];
int sizeofpm = [pm_participants count];
NSMutableArray *participants =[[NSMutableArray alloc] init];
NSLog(#"size of am: %i", sizeofam);
if(sizeofam > 0 && sizeofpm > 0){
for(int i = 0; i < sizeofam; i++){
NSDictionary *ampart_dict = [am_participants objectAtIndex:i];
NSString *ampart_email = [ampart_dict objectForKey:#"email"];
NSString *ampart_email_extra = [#"am" stringByAppendingString:ampart_email];
[participants addObject:ampart_dict];
}
for(int i = 0; i < sizeofpm; i++){
NSDictionary *pmpart_dict = [pm_participants objectAtIndex:i];
NSString *pmpart_email = [pmpart_dict objectForKey:#"email"];
NSString *pmpart_email_extra = [#"pm" stringByAppendingString:pmpart_email];
[participants addObject:pmpart_dict];
NSLog(#"%#", participants);
}
int sizeofparticipants = [participants count];
for(int i = 0; i < sizeofparticipants; i++){
NSString *participanti = [[participants objectAtIndex:i] objectForKey:#"email"];
for(int j = 0; j < sizeofparticipants; j++){
NSString *participantj = [[participants objectAtIndex:j] objectForKey:#"email"];
if([participanti isEqualToString:participantj]){
NSLog(#"Participant j: %#", participantj);
[participants removeObjectAtIndex:j];
sizeofparticipants = [participants count];
}
}
}
Basically I need to merge together the two arrays am_participants and pm_participants in to one array of participants. Each object in the array is a dictionary full of various values such as name, email etc. I then have to remove any duplicate values from the list (for example if someone is in the am list and pm list. I am doing that by removing values if their email address is the same in either list.
After this I need to display the new array of participants called 'participants' in one list by populating the cells with the list of participants.
I have set the number of cells to 30.
However The cells get populated with different names randomly when scrolling down the list as thought the array positions are being moved around aswell as duplicate values appearing.
Can someone explain what I am doing wrong?
Thanks!
the cell assignment code is not mentioned above but i assume that you are assigning values from participants as your cell's content as explained. The problem is you are creating your array inside cellForRowAtIndexPath:. This method gets called every time a new cell is to be displayed, thus, you are recreating your participants for every row. You need to move the code you have above to viewDidLoad or viewDidAppear.
I have used a number of grouped tables tied to core data managed objects, where the sectionNameKeyPath value is used to identify the attribute in the data that should be used to denote sections for the table.
But how do I indicate the "sectionNameKeyPath equivalent" when I have a table that is being use to present an NSMutableArray full of objects that look like this:
#interface SimGrade : NSObject {
NSNumber * scoreValue;
NSString * commentInfo;
NSString * catName;
}
I would like to have sections defined according to the "catName" member of the class.
Consider, for example that my mutablearray has 5 entries where the 5 "catName" values are "Blue", "Blue", "Blue", "Red", and "Red". So I'd want the number of sections in the table for that example to be 2.
So, what I would 'like to do' could be represented by the following pseudo-code:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return (The number of unique catNames);
}
Note: My interest in doing this is not so much for displaying separate sections in the table, but rather so that I can more easily calculate the sums of scoreValues for each category.
<<<<< UPDATE >>>>>>>>>
Joshua's help, as documented in his response has been right on. Here are the two new handlers for number of sections and number of rows per section...
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
NSMutableSet *counter = [[NSMutableSet alloc] init];
[tableOfSimGrades enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop) {
[counter addObject:[object catName]];
}];
NSInteger cnt = [counter count];
[counter release];
NSLog(#">>>>> number of sections is -> %d", cnt);
return cnt;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
NSMutableDictionary *counter = [[NSMutableDictionary alloc] init];
NSMutableArray *cats = [[NSMutableArray alloc] init];
__block NSNumber *countOfElements;
[tableOfSimGrades enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop) {
// check the dictionary for they key, if it's not there we get nil
countOfElements = [counter objectForKey:[object catName]];
if (countOfElements) {
// NSNumbers can't do math, so we use ints.
int curcount = [countOfElements intValue];
curcount++;
[counter setObject:[NSNumber numberWithInt:curcount] forKey:[object catName]];
NSLog(#">>>> adding object %d to dict for cat: %#", curcount, [object catName]);
} else {
[counter setObject:[NSNumber numberWithInt:1] forKey:[object catName]];
[cats addObject:[object catName]];
NSLog(#">>>>> adding initial object to dict for cat: %#", [object catName]);
}
}];
countOfElements = [counter objectForKey:[cats objectAtIndex: section]];
int catcount = [countOfElements intValue];
[counter release];
[cats release];
return catcount;
}
My current issue with this routine now lies in the following function... It is ignorant of any sections in the nsmutableArray and so for each section, it starts at index 0 of the array instead of at the 0th element of the appropriate section.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
// Configure the cell...
SimGrade *tmpGrade = [[SimGrade alloc] init];
tmpGrade = [tableOfSimGrades objectAtIndex: indexPath.row];
cell.detailTextLabel.text = [NSString stringWithFormat:#"Category: %#", tmpGrade.catName];
// [tmpGrade release];
return cell;
}
How do I transform the "indexpath" sent to this routine into the appropriate section of the mutableArray?
Thanks,
Phil
You could do something like this:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
NSMutableSet *counter = [[NSMutableSet alloc] init];
[arrayOfSims enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop) {
[counter addObject:object.catName];
}];
NSInteger cnt = [counter count];
[counter release];
return cnt;
}
you'd probably want to memoize that, for performance reasons (but only after profiling it).
--- EDIT ---
You can use an NSMutableDictionary, too, to get counts of individual categories.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
NSMutableDictionary *counter = [[NSMutableDictionary alloc] init];
__block NSNumber *countOfElements;
[arrayOfSims enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop) {
// check the dictionary for they key, if it's not there we get nil
countOfElements = [counter objectForKey:object.catName];
if (countOfElements) {
// NSNumbers can't do math, so we use ints.
int curcount = [countOfElements intValue];
curcount++;
[counter setObject:[NSNumber numberWithInt:curcount] forKey:object.catName];
} else {
[counter setObject:[NSNumber numberWithInt:1] forKey:object.catName];
}
}];
NSInteger cnt = [counter count];
// we can also get information about each category name, if we choose
[counter release];
return cnt;
}
Problem: When I click the delete button for a given table/section row, i get the following error: "*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (4) must be equal to the number of rows contained in that section before the update (4), plus or minus the number of rows inserted or deleted from that section (0 inserted, 1 deleted).'"
From other posts I have read about this symptom, I gather I am suppose to be manually removing an element in my datasource array, but not sure how to access the section's array inside this method:
// COMMIT EDITING STYLE
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"indexPath: %#", indexPath);
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the row from the data source
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates]; // throws error here
[tableView reloadData];
}
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
}
}
I think the complication for this situation arises due to the fact that the plist (FormEntries.plist) I am pulling data from holds user input for all sorts of things all through out my app, thus I am having to call and filter it for every section. This works fine to populate the UITableView and all of it's sections, but because a new filtered array is being created for and inside each section, I'm not sure how to ever access it again in order to remove the element, thus rectifying the above error message. Here is how I am loading the data for each table section:
// CELL FOR ROW AT INDEXPATH
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath (NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
NSNumber *numScreenId = [[arrayOfModulesScreens objectAtIndex: indexPath.section] objectForKey: #"id"];
NSMutableArray *arrayRecords = [epFrameWork selectPlist: #"FormEntries" filterByKey: #"screen_id" keyValue:numScreenId];
NSString *strTitle = [[arrayRecords objectAtIndex: indexPath.row] objectForKey: #"storage_string"];
cell.textLabel.text = strTitle;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
return cell;
}
-- Not sure if this will help diagnose things, but here it is none the less ---
// TITLE FOR HEADER IN SECTION
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return [[arrayOfModulesScreens objectAtIndex: section] objectForKey: #"screen_title"];
}
// NUMBER OF SECTIONS IN TABLE VIEW
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [arrayOfModulesScreens count];
}
// NUMBER OF ROWS IN SECTION
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSNumber *numScreenId = [[arrayOfModulesScreens objectAtIndex: section] objectForKey: #"id"];
NSMutableArray *arrayRecords = [epFrameWork selectPlist: #"FormEntries" filterByKey: #"screen_id" keyValue:numScreenId];
int rowCount = [arrayRecords count];
return rowCount;
}
What is the best approach to handle this situation or to resolve the above posted error message?
-- UPDATE --
So here is how I'm trying to identify which plist record to delete, assuming that's what I need to do to resolve the original error:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
int g = indexPath.row;
int count = -1;
UITableViewCell *tvc = [[UITableViewCell alloc] init];
for(id element in tableView.subviews) {
if([element isKindOfClass:[UITableViewCell class]]) {
count +=1;
NSLog(#"g: %d - count: %d", g , count);
if(count == g) {
tvc = element;
NSLog(#"tvc: %# - UID: %# - g: %d - count: %d", tvc, tvc.detailTextLabel.text, g , count);
}
}
}
My logic here was to set a hidden unique identifier on tvc.detailTextLabel.text in the cellForRowAtIndexPath method, which in turn would let me know which record from the plist to filter and delete by calling [array removeObjectAtIndex:uid] where array is my filtered plist array. Only problem now is that tvc in the NSLog always returns the record at index 0, not the row that holds the delete button I click.
NSLog returns: tvc: < UITableViewCell: 0x713e2c0; frame = (0 30; 320 44); text = 'Church A'; autoresize = W; layer = < CALayer: 0x7113e70 > > - UID: -237206321 - g: 3 - count: 3. So why would tvc return the index 0 when it was index 3 I clicked the delete button?
Is this just becoming a clustered mess or is there a cleaner solution? But ya, still stumped.
This error most definitely relates to your mishandling the data that you are trying to load to your table. I found that the easiest and safest way to handle modifying table content is to do something along those lines, with the necessary adjustments (within tableView:commitEditingStyle:)
//REMOVE A CELL FROM A SECTION
[yourTable beginUpdates];
[yourTable deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationBottom];
[yourTable endUpdates];
[yourTable reloadData];
In addition you need to make sure that your array is properly updated to have the changes reflected in the table.
This is how I was finally able to resolve the issue:
I changed all of this crap:
int g = indexPath.row;
int count = -1;
UITableViewCell *tvc = [[UITableViewCell alloc] init];
for(id element in tableView.subviews) {
if([element isKindOfClass:[UITableViewCell class]]) {
count +=1;
NSLog(#"g: %d - count: %d", g , count);
if(count == g) {
tvc = element;
NSLog(#"tvc: %# - UID: %# - g: %d - count: %d", tvc, tvc.detailTextLabel.text, g , count);
}
}
}
to one simple line:
UITableViewCell *cell = [[self tableView] cellForRowAtIndexPath:indexPath];
This allowed me to identify the cell I was working with. So my final code that worked looks like this:
// COMMIT EDITING STYLE
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
UITableViewCell *cell = [[self tableView] cellForRowAtIndexPath:indexPath];
[epFrameWork deleteRecordFromPlist:#"FormEntries" uid:cell.detailTextLabel.text];
[tableView reloadData];
}
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
}
}
-(void) deleteRecordFromPlist:(NSString *)plist uid:(NSString *)uId {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *tmpFileName = [[NSString alloc] initWithFormat:#"%#.plist", plist];
NSString *path = [documentsDirectory stringByAppendingPathComponent:tmpFileName];
NSMutableArray *array = [[NSMutableArray alloc] initWithContentsOfFile:path];
NSDictionary *dict = [[NSDictionary alloc] init];
NSString *tmpUid;
for(int i=0; i < [array count]; i++) {
dict = [array objectAtIndex:i];
tmpUid = [dict valueForKey:#"uid"];
if([tmpUid isEqualToString:uId]) {
[array removeObjectAtIndex:i];
[array writeToFile:path atomically:YES];
}
}
}
I have a UITable with a datasource that is set to a 'copy' of the original data. I use this copy to displayed filtered results (either 'All' or only those that are 'checked' with a UISwitch in each row). My logic for doing the filtering is here:
-(void)filterItems {
[tableData removeAllObjects];
if (checkedOrNotSwitch.selectedSegmentIndex == 0) {
[tableData addObjectsFromArray:self.defaultChecklistItemsArray];
} else {
for (NSMutableDictionary *sectionDict in self.defaultChecklistItemsArray) {
NSMutableArray *newItemsArray = [[NSMutableArray alloc] init];
[newItemsArray removeAllObjects];
for (NSMutableDictionary *itemDict in [sectionDict objectForKey:#"categoryItems"]) {
if ([[itemDict objectForKey:#"isComplete"] boolValue]) {
[newItemsArray addObject:itemDict];
}
}
if ([newItemsArray count] > 0) {
NSMutableDictionary *newSectionDict = [[NSMutableDictionary alloc] initWithDictionary:sectionDict];
[newSectionDict setObject:newItemsArray forKey:#"categoryItems"];
[tableData addObject:newSectionDict];
[newSectionDict release];
}
}
}
[checklistTable reloadData];
}
The filtering itself now works correctly. In my custom cell, each row has a UISwitch. The switch runs this function when its changed:
-(IBAction) switchValueChanged{
NSIndexPath *indexPath = [(UITableView *)self.superview indexPathForCell: self];
[self.parentController updateCompletedStatusAtIndexPath:indexPath toStatus:isCompleted.on];
}
The function above is in the class for the tableviewcell itself. The function I call in the superview is this:
-(void)updateCompletedStatusAtIndexPath:(NSIndexPath *)indexPath toStatus:(BOOL)status{
NSUInteger section = [indexPath section];
NSUInteger row = [indexPath row];
NSMutableDictionary *currentsection = [[NSMutableDictionary alloc] initWithDictionary:[tableData objectAtIndex:section]];
NSMutableArray *itemsArray = [[NSMutableArray alloc] initWithArray:[currentsection objectForKey:#"categoryItems"] copyItems:YES];
NSMutableDictionary *tempDict = [[NSMutableDictionary alloc] initWithDictionary:[itemsArray objectAtIndex:row]];
NSLog(#"BOOL = %#\n", (status ? #"YES" : #"NO"));
[tempDict setValue:[NSNumber numberWithBool:status] forKey:#"isComplete"];
[itemsArray replaceObjectAtIndex:row withObject:tempDict];
[currentsection setValue:itemsArray forKey:#"categoryItems"];
[tableData replaceObjectAtIndex:section withObject:currentsection];
[tempDict release];
[itemsArray release];
[currentsection release];
[checklistTable reloadData];
}
Before I implemented the filtering and used the logic above on self.defaultChecklistItemsArray directly, it worked and saved the data when the switch was flipped.
Now when I first enter the app, it loads the array of data from nsuserdefaults. I navigate to the view with the table and it displays the data correctly there with the UISwitches all in the correct position given the data (some on, some off). When I tap one of the switches, then click the segmentedcontrol that does the filtering, the data reverts back to the state it was loaded in, implying that the flipping of the switch did not actually effect the data at all (even though I don't think I did a deep copy anywhere here so I figured it should be doing the right thing). Any suggestions?