I have a UITableViewController that is displaying rows from my NSFetchedResultsController. My has several sections, with the rows being ordered within each section, using an 'order' field.
I'm using fairly bog-standard code to handle the moving of rows. The rows moves are restricted to the section they are contained within. When a row rearrange takes place, I use the following code..
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
self.fetchedResultsController.delegate = nil;
NSMutableArray *myImages = [[self.fetchedResultsController fetchedObjects] mutableCopy];
// Grab the item we're moving.
NSManagedObject *myImage = [[self fetchedResultsController] objectAtIndexPath:sourceIndexPath];
// Remove the object we're moving from the array.
[myImages removeObject:myImage];
// Now re-insert it at the destination.....here lies the problem???
[myImages insertObject:myImage atIndex [destinationIndexPath row]];
// All of the objects are now in their correct order. Update each
// object's displayOrder field by iterating through the array.
int i = 0;
for (MyImage *myImage in myImages)
{
myImage.order = [NSNumber numberWithInt:i];
i++;
}
// Save the data here!
self.fetchedResultsController.delegate = self;
}
(Some variable names have been changed to protect the innocent!!!)
This works quite well but doesn't always perform as expected. The reason being (I think) is I'm removing the object from the array successfully, but insertion again is wrong because it uses the absolute row index of the whole array, without allowing for the fact that this may be in (say) the second or third section of the UITableView/NSFetchedResultsController.
So, how can I put an object back into my mutable array (which is linear) into my the correct index, given the destination indexPath is two-dimensional? (I could probably count previous rows/section) but wonder if there's a prettier way.)
Thanks
Related
I want to create 2 arrays, and populate my table view with that arrays. Then, if user click on a cell, that contain object from first array, i want to perform transition to my detail controller "one". Therefore, if user tap on cell, that contain text from second array, i want to perform segue for detail controller "two".
How to achieve that? I can't put tag on array objects and check it.
Edit: or, if NSDictionary suitable for that case, i could use them instead.
instead of having an array of data elements, you could have an array of MyDataClass, which has attributes for your data, and to identify the source.
You can use a single array to populate the table, and as suggested have different methods for populating the table cell based on the source.
Another way to do this would be create a third array with NSDictionary objects with two keys 'tag' and 'data'. 'tag' key will hold the information about which array and 'data' key will hold data from the array.
NSMutableArray *tableArray = [NSMutableArray arrayWithCapacity:0];
for (id obj in array1) {
[tableArray addObject:#{#"tag":#1, #"data":obj}];
}
for (id obj in array2) {
[tableArray addObject:#{#"tag":#2, #"data":obj}];
}
Then use this new array to populate your table
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
....
id obj = [[tableArray objectAtIndex:indexPath.row] objectForKey:#"data"];
....
}
and on didSelectRowAtIndexpath you can check value for 'tag' key to check whether it is from array1 or array2
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
....
NSNumber *tag = [[tableArray objectAtIndex:indexPath.row] objectForKey:#"tag"];
if (tag.intValue == 1) {
//controller 1
}
else if (tag.intValue == 2) {
//controller 2
}
....
}
There are multiple ways of looking to this problem. Firstly, you have to understand that you can populate a table view with only a single array; however this array can be made from multiple arrays.
Without getting ourselves dirty into multiple data structures that might provide a lot of redundancy than efficiency, a simple way would be to check for the array number in tableView:didSelectRowAtIndexPath:.
Example:
- (void) tableView: (UITableView *) tableView didSelectRowAtIndexPath: (NSIndexPath *) indexPath
{
<Type> obj = tableViewArray[indexPath.row];
if (obj in array1)
{
// ...
}
else
{
// ...
}
}
The tableViewArray is probably array1+array2.
I am trying to suppress a NSManagedObject from a table view.
I have a TableView controller that subclasses UITableViewController and that owns an additional property of type myContainer as well as an array of items.
#property (nonatomic, strong) myContainer * parentContainer;
#property(nonatomic, strong) NSArray * allItems;
myContainer is a NSManaged object that contains a NSSet * of objects of type myItem :
#interface myContainer : NSManagedObject
#property (nonatomic, strong) NSSet *subItems;
I have implemented the delete function like this :
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSManagedObjectContext *context = [[Catalog sharedCatalog] managedObjectContext];
myItem *selectedItem = [self.parentContainer.subItems allObjects][indexPath.row];
[context deleteObject:selectedItem];
[self viewWillAppear:NO];
[self.view setNeedsDisplay];
}
}
(note that the instruction [context deleteObject:selectedItem] gives me a warning "Incompatible pointer types sending 'myObject *' to parameter of type 'NSManagedObject ' ; but I doubt this is relevant for what follows)
The method viewWillAppear is like this :
- (void)viewWillAppear:(BOOL)animated {
self.allItems = [self.parentContainer.subItems allObjects];
NSLog(#"%lu objects",(unsigned long)[self.allItems count]);
[super viewWillAppear:animated];
[self.tableView reloadData];
}
For the purpose of understanding what happens I also subclassed cellForRowAtIndexPath
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// .. //
myItem * selectedItem = self.allItems[indexPath.row];
NSLog(#"%lu : %#",indexPath.row,[selectedItem description]);
}
Now assume I first load my table and it contains 3 objects. I receive the log :
3 objects
0 : item0
1 : item1
2 : item2
I swipe and delete the second item. The view updates but the corresponding cell does not disappear; it just becomes empty. The log reads like this :
3 objects
0 : item0
1 : (null)
2 : item2
Now I do anything else that will reload the view : either go the previous view and come back, go to a further view and come back, or delete a second object. Then everything is fine as expected and the log is :
2 objects
0 : item0
1 : item2
I have seen the two following questions that seem strongly related but they did not receive any answer that seemed to solve the problem : tablerow delete but still showon in table view and commitEditingStyle: delete data but continue to display row
EDIT I also know that there is a better way to suppress cell calling [tableView deleteRowsAtIndexPath ..] (and it works here) but I would really like to understand the output that I get and what coreData is doing with the objects behind my back...
I can't get my head around what is happening here. I tried several variations on the previous code but nothing seemed to solve my problem.
From what I see, I assume myContainer has a to-many relationship with myItem. That's good, because Core Data will automatically remove objects from that relationship after myItems objects are deleted, but not immediately. Unlike some on here have said, you don't have to save the context or refresh anything for these changes to be reflected in the relationship, you just need to call processPendingChanges on the context after your deletion, like so:
[context deleteObject:selectedItem];
[context processPendingChanges];
processPendingChanges will cause Core Data to do any relationship updates it still needs to do and will also fire any core data related notifications that might still be pending.
You do also need to make sure your allItems array is re-populated when these changes occur (which you do not currently appear to be doing) as Core Data will not deallocate objects that have been deleted or remove them from ordinary arrays. It'll just mark it as deleted and leave it taking up space in your array. Similar to what Erakk said in his answer, you should probably encapsulate this array re-population into the same method as your table view reload. You could also call processPendingChanges here instead of after your deletion to make sure that any and all pending changes are reflected in the relationship before pulling the objects out of the relationship. You don't need to manually update the subItems relationship in myContainer. That's Core Data's job.
- (void)reloadTableView {
[self.parentContainer.managedObjectContext processPendingChanges];
self.allItems = [[NSMutableArray alloc] initWithArray:[self.parentContainer.subItems allObjects]];
[self.tableView reloadData];
}
As some others have said, you definitely don't want to be calling viewWillAppear: directly. You should move your code to a helper that is called from both places.
This line is also a potential issue:
myItem *selectedItem = [self.parentContainer.subItems allObjects][indexPath.row];
subItems is an unordered, to-many relationship, which means that you're not guaranteed to get the item that you really expect out of the array here, as the "allItems" method on NSSet could theoretically come back in any order. You should either store these objects in a (presumably sorted) array and access that instead, or you can change your subItems relationship into an ordered relationship and then change the property to an NSOrderedSet.
Hope this helps!
To me, it looks like you are not updating the data array. You deleted item 1, but the array still has a reference to that NSManagedObject, which probably was tagged by coreData as "Deleted". That happens because the only part of the code you set the array is on viewWillAppear.
Instead, you should re-fetch the array whenever you delete an item and then reload the data.
This is also the reason whenever you go back to the previous/further view and come back, it works. because viewWillAppear is being called and updating the array.
UPDATE:
It seems to me that you get the objects from your parentContainer. this could mean that the parentContainer needs to be aware that you deleted an item to update the array too. I suggest you create a delegate, send a notification to it, or just create a public method in your parentContainer, and let this viewController call in whenever you reload the tableView.
The way i'd do is to encapsulate the logic to reload the tableView:
- (void)reloadTableView {
[self.parentContainer reloadSubItems];
self.allItems = [self.parentContainer.subItems allObjects];
[self.tableView reloadData];
}
then change viewWillAppear to call it:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self reloadTableView];
}
and instead of explicitly calling the viewWillAppear in the commitEditingStyle:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSManagedObjectContext *context = [[Catalog sharedCatalog] managedObjectContext];
myItem *selectedItem = [self.parentContainer.subItems allObjects][indexPath.row];
[context deleteObject:selectedItem];
[self reloadTableView];
//you dont need to call setNeedsDisplay here to update the
//tableView unless you do other stuff related to view frames
//and the like, for example, you changed the tableView's
//frame. in this case you would call setNeedsLayout.
}
}
You would create the reloadSubItems in your parentContainer and add the fetching logic into it.
UPDATE 2:
instead of editing your parentContainer, you could just remove the item being deleted from the array. First you need to change allItems to be of type NSMutableArray instead of NSArray.
your reloadTableView method would look like this:
- (void)reloadTableView {
[self.parentContainer reloadSubItems];
self.allItems = [[NSMutableArray alloc] initWithArray:[self.parentContainer.subItems allObjects]];
[self.tableView reloadData];
}
and the commitEditingStyle:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSManagedObjectContext *context = [[Catalog sharedCatalog] managedObjectContext];
myItem *selectedItem = [self.parentContainer.subItems allObjects][indexPath.row];
[self.allItems removeObject:selectedItem];
[context deleteObject:selectedItem];
[self reloadTableView];
}
}
This way you don't need to tell the parentContainer to update the items, since you will be deleting the item from the array yourself.
UPDATE 3:
according to the documentation on deleteObjects:, "When changes are committed, object will be removed from the uniquing tables. If object has not yet been saved to a persistent store, it is simply removed from the receiver."
So you need to save the context for your deletion to be committed to the store, or else the object will simply be flagged as "deleted"
I'm starting to get confused. I'm using a FetchedResultsController for my tableview data. In each cell I have a button and a textfield tagged with the indexPath.Row in the cellForRowAtIndexPath method:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//Data model and cell setup
static NSString *CellIdentifier = #"MainCategoryCell";
MainCategoryTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
MainCategory *mainCategory = [self.fetchedResultsController objectAtIndexPath:indexPath];
/* ... */
cell.title.tag = indexPath.row;
cell.iconButton.tag = indexPath.row;
return cell;
}
Now my row move method is a bit more complicated for the Fetched Results controller. However I'm pretty sure the tags don't get updated after the moving. Is that normal and is the cellForRow method only called after creating a new cell? Do I have to update the tags myself in the move method? And how could I access there the tag properties of the objects within the cells?
- (void)tableView:(UITableView *)tableView
moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath
toIndexPath:(NSIndexPath *)destinationIndexPath;
{
// Process the row move. This means updating the data model to correct the item indices.
//reordering has been defined in the CoreDataViewController so the
//FetchedResultsController doesn't mess up the reordering since he would update
//the fetched results permanently while reordering
self.reordering = YES;
//Makes only a mutable copy of the array, but NOT the objects (references) within
NSMutableArray *fetchedResults = [[self.fetchedResultsController fetchedObjects] mutableCopy];
// Grab the item we're moving
NSManagedObject *resultToMove = [self.fetchedResultsController objectAtIndexPath:sourceIndexPath];
// Remove the object we're moving from the array.
[fetchedResults removeObject:resultToMove];
// Now re-insert it at the destination.
[fetchedResults insertObject:resultToMove atIndex:[destinationIndexPath row]];
// All of the objects are now in their correct order. Update each
// object's displayOrder field by iterating through the array.
int i = 1;
for (MainCategory *fetchedResult in fetchedResults)
{
fetchedResult.position = [NSNumber numberWithInt:i++];
}
// Save
NSError *error = nil;
[self.budgetDatabase.managedObjectContext save:&error];
// re-do the fetch so that the underlying cache of objects will be sorted
// correctly
[self.fetchedResultsController performFetch:&error];
self.reordering = NO;
}
Yes it is normal that the tags won't get updated when you move cells. Since all cells have the possibility of being shifted, simply reload the table view to have it regenerate the tags for your button and text box.
[tableView reloadData];
After using [UITableView deleteSections:withRowAnimation:] on a section which is out of view - the section header remains visible.
On this image, we see the visible part of the tableview
On the next image, we see the whole tableview - AISLE 2 is hidden until the user scrolls down, it contains only one row:
When I scroll down and delete the last row, AISLE 2 section header remains visible, even though I used deleteSections. if I delete a row from AISLE 1, the section header remains on the same place, and by scrolling down I can still see it.
Furthermore, when trying to scroll down so that AISLE 2 header is in the view, the UI acts as AISLE2 is NOT part of the tableview, and immediately scrolls me back up. Which means - this is a garbage view that is obviously not part of the table, since I removed it. for some reason, iOS doesn't remove this view, but de-associates it from the table.
Any ideas?
Try [tableview reload] with making numberofsections as 1.
Where is your data coming from and how do you know how many sections there are at the start? I think your problem can easily be resolved by explaining this.
It looks like you may have a multidimentional nsmutablearray where each index is an aisle and each object contains the products for that isle? Or you may have a different array for each aisle?
When you delete a cell, simply check how many cells are left, and call [self.tableView reloadData];
For (hypothetical) example;
if you have arrays of your Aisles:
NSMutableArray *aisleOne = [[NSMutableArray alloc] initWithObjects:#"Product1", #"Product2", nil];
NSMutableArray *aisleTwo = [[NSMutableArray alloc] initWithObjects:#"Product1", #"Product2", nil];
NSMutableArray *aisleThree = [[NSMutableArray alloc] initWithObjects:#"Product1", #"Product2", nil];
NSMutableArray *aisleFour = [[NSMutableArray alloc] initWithObjects:#"Product1", #"Product2", nil];
and you add them to one array:
NSMutableArray *aisleArray = [[NSMutableArray alloc] initWithObjects:aisleOne, aisleTwo, aisleThree, aisleFour, nil];
then, call this code when you delete a cell. It will remove all empty Aisles from the aisleArray (which needs to be globally defined):
NSMutableArray *tempArray = [[NSMutableArray alloc] initWithArray:aisleArray];
for (int i=0; i<[tempArray count]; i++) {
if ([[tempArray objectAtIndex:i] count] == 0) {
[aisleArray removeObjectAtIndex:i];
}
}
[self.tableView reloadData];
For this to work, these two methods should be:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [aisleArray count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [[aisleArray objectAtIndex:section] count];
}
(untested)
Just get the table haderview for that section and remove it from it's superview and tableView is not managing it.
UIView *mysteriousView = [tableView headerViewForSection:indexPath.section];
[tableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationAutomatic];
[mysteriousView removeFromSuperview];
Hope this helps!
Few yers later I'm facing same issue... Deleting last section doesn't remove last section header from table view.
I noticed however that the remaining header has frame exactly just below normal table view content.
So the workaround (in swift but you can easily translate it to Objective C) that worked for me is something like this:
DispatchQueue.main.async {
let unwantedViews = self.tableView.subviews.filter{ $0.frame.minY >= self.tableView.contentSize.height}
unwantedViews.forEach{ $0.isHidden = true }
}
.
Hello,
I have the following code to set the re-ordering of the tableView.
#pragma mark Row reordering
// Determine whether a given row is eligible for reordering or not.
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
// Process the row move. This means updating the data model to <span id="IL_AD6" class="IL_AD">correct</span> the item indices.
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath
toIndexPath:(NSIndexPath *)toIndexPath {
NSString *item = [arr objectAtIndex:fromIndexPath.row];
[arr removeObject:item];
[arr insertObject:item atIndex:toIndexPath.row];
}
But when I reload the TableView (note not a TableViewController) The order is set back to what it was before I changed anything.
Now when I setup the editing for this view (the delete buttons) I had to add this line:
[self.mainTableView setEditing:YES animated:YES];
before it appeared in my view.
Is there a similar thing I need to set with the re-ordering of the table view for it to save?
The data is fetched from a Core Data database.
In order to persist the order of the objects I'm afraid you will need to add an attribute for that to your data model. Just use an int / [NSNumber] and recompute them in a loop when a table view row is moved.
When you reload the data, are you re-populating that array with the result of a fetch from your Core Data entity? If so, the changes you made to the array (which I assume is the data source for the TV) will be blown away. If you want those changes to stick, you would need to update the model somehow to make sure the re-population of the array results in the array being indexed the way you need.