I have a UITableView powered by an NSFetchedResultsController and am trying to properly support reordering.
Inside moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath I respond to the reordering by updating an order value on the NSManagedObjects and save the context like this:
[self.fetchedResultsController.managedObjectContext save:nil];
So now all objects have the correct order value and the results controller's NSFetchRequest sorts by that value correctly.
This does appear to work, however when I scroll up and down in the table view, the old values are being displayed. So it looks like when I use objectAtIndexPath on the NSFetchedResultsController, it doesn't return the latest object.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// ... cell generation code ...
NSManagedObject *obj = [self.fetchedResultsController objectAtIndexPath:indexPath];
[cell.textLabel setText:sideEffect.name];
}
If I navigate away and then return, the cells are in the correct order.
After my call to save, do I need to:
Refresh the NSFetchedResultsController
Invalidate a cache
Refresh/reset something on the UITableView?
The solution was to reinitialise the fetch controller after the move modifications had taken place. Since I had a special getter method generating the controller (and performing the first fetch), I could just set the instance variable to nil to cause it to be regenerated next time it was needed.
Related
Below is the sequences how my app is loading data into my UITableView:
UITableView loads from empty array.
App fetches data asynchronously.
After data is fetched and processed, app calls UITableView reloadData.
The problem I'm currently encountered is, I need to slightly slide the UITableView manually, in order to enable UITableView display the fetched data.
Currently I'm using iOS simulator to do the testing.
Is this considered normal? Is there anything I can do to display the data without a request from user to slide it?
-- EDIT --
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
NSString *tableCellIdentifier = #"TempTableViewCell";
TempTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:tableCellIdentifier forIndexPath:indexPath];
int row = [indexPath row];
YoutubeVideo *currentVideo = [videoArray objectAtIndex:row];
cell.TempLabel.text = currentVideo.VideoTitle;
[cell.TempImage setImageWithURL:[NSURL URLWithString:currentVideo.VideoThumbnailURL] placeholderImage:[UIImage imageNamed:#"Icon-Small"]];
cell.TempLabelDate.text = currentVideo.VideoDate;
return cell;
}
If I understand your question properly, then...
I would guess that when you call [tableView reloadData], you are not on the main thread. Try this:
dispatch_async(dispatch_get_main_queue(), ^{
[tableView reloadData];
});
Edit with clarification:
The reason for this is that your network request is probably occurring on a different thread, and, once you've finished and you reload the table, you are still on this background thread. When performing UI updates, you need to do it on the main thread, otherwise you get weird stuff happening , like you are seeing here.
The code above will perform the table update back on the main thread.
I've implemented the
- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
delegate method and within that I implement this piece of code
UITableViewHeaderFooterView *header = [[UITableViewHeaderFooterView alloc] initWithReuseIdentifier:#"paymentFormHeader"];
header = (UITableViewHeaderFooterView *)view;
Then I try the and assign a UIView from the header with
UIView *header = [self.tableView dequeueReusableHeaderFooterViewWithIdentifier:#"paymentFormHeader"];
and get a null value in return. This is my first time using this method so I'm probably not understanding it correctly and I noticed that it didn't ask for an indexPath. Any help with this would be greatly appreciated. Thanks.
There are a few issues here. The first is that you're not using the right method to set up the header. The method you're using (..willDisplayHeaderView..) is there to let you know when a header you've already set up is about to be displayed so you can do any additional setup or tracking after that point.
You will want to implement 2 methods to get this working:
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
and
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
Your next issue is how you are trying to dequeue the view. You either need to check for a nonexistent dequeued view and initialize one manually with that reuse identifier, or just register the appropriate class in advance. I recommend registering it in advance. So to fix this, in viewDidLoad or at some point after your UITableView has been initialized, register the class like this:
[myTableView registerClass:[UITableViewHeaderFooterView class] forHeaderFooterViewReuseIdentifier:#"paymentFormHeader"];
That will make sure you will always get a valid initialized view of that type for that reuse identifier when you try to dequeue it.
I created a viewController with a tableView and set the identifier of the only column to "name". Then I created an arrayController, bound it to a NSManagedObjectContext and set the right entity name.
When I now load the viewController, the tableView does display the correct amount of row. But unfortunately the cells do not contain the value of the NSManagedObjects value for the key name.
What do I have to implement in my NSManagedObject subclass or in the viewController (which is the tableViews viewController)?
I'd like to show you some code, but I don't know what could be helpful here, because it's more an conceptional question... So I'll post code as requested in comments.
UPDATE
This is the code I'm using to bind the arrayController tho the tableView:
[_tableView bind:NSContentBinding toObject:_arrayController withKeyPath:#"arrangedObjects.name" options:nil];
To inspect what the tableView gets, I added this line (after adding property called "content"):
[self bind:NSContentBinding toObject:_arrayController withKeyPath:#"arrangedObjects.name" options:nil];
In the setter I got an array containing NSString instances. But the tableView still does not display any values...
I finally used the standard NSTableViewDataSource protocol:
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
return [[_arrayController arrangedObjects] count];
}
- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
return [[[_arrayController arrangedObjects] objectAtIndex:row] valueForKey:[tableColumn identifier]];
}
I think this is a solid solution, though. I might became obsessed by the -bind:toObject:withKeyPath:options: idea.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
if ([BGSearchParameter defaultSearchParameters].fetchedResults.count==0)
{
PO(#([self.tableView numberOfRowsInSection:0]));
PO(#([self tableView:nil numberOfRowsInSection:0]));
PO(#(indexPath.row));
while (false);
}
This is the result
self.tableView numberOfRowsInSection:0]): 20
#([self tableView:nil numberOfRowsInSection:0]): 0
Now, I do not know who call that cellForRowAtIndexPath.
Here is the screenshot
The debuggin windows shows that the main thread somehow call cellForRowAtIndexPath.
I usually fix this issue by returning some random UITableViewCell that's never displayed. Howerver, I think it's a bug.
What functions can call cellForRowAtIndexPath anyway besides tableReload and why it doesn't call
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
first?
It seems that my tableView still think there are 20 rows even though there are only 0 rows.
The issue seems to be your tableview is not refreshing. Please include this line in your code so that it will refersh each time:-
[yourTableView reloadData];
You can use the parameter "tableView" of this method to check whether the right instance of "UITableView" is called. For example, in the delegate method" - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section" add these code:
if(tableView== youwanttocheckTableView) {
//do what you want to do
}
You can also check which TableView instance have set its delegate equals "self" in this class.
I have a somewhat weird issue, and I can't quite figure out what am I missing. I have a UITableView which is editable in place (i.e. when my UI is loaded, I send my table the setEditing:YES animated:YES message). The last row in the table is intended to be the "Add New" row. All rows except the last row in my table can be moved around. None of the rows can be deleted.
The rows show up correctly, and the grabbers shows up on the right side of all rows except the last row (as intended). The problem is that I am unable to move the rows. When I tap on the grabber to move the row, it kind of jiggles in place, but I can't drag it up or down. Here's the relevant snippet of code:
- (UITableViewCellEditingStyle)tableView:(UITableView *)aTableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewCellEditingStyleNone;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == [self.itemArray count] ) {
return NO;
}
return YES;
}
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == [self.itemArray count]) {
return NO;
}
return YES;
}
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
Item *item = [self.itemArray objectAtIndex:fromIndexPath.row];
[self.itemArray removeObjectAtIndex:fromIndexPath.row];
[self.itemArray insertObject:item atIndex:toIndexPath.row];
}
- (NSIndexPath *)tableView:(UITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath {
if ([proposedDestinationIndexPath row] < [self.itemArray count]) {
return proposedDestinationIndexPath;
}
NSIndexPath *betterIndexPath = [NSIndexPath indexPathForRow:[self.itemArray count]-1 inSection:0];
return betterIndexPath;
}
On trying to debug, it seems that the tableView:moveRowAtIndexPath: gets called almost immediately even as I am holding on to the grabber (i.e. I have not lifted my finger yet). Moreover, my tableView:targetIndexPathForMoveFromRowAtIndexPath:proposedDestinationIndexPath: does not get invoked at all.
Any thoughts on what am I doing wrong? Any suggestions on what I should try to fix this issue?
I had the same problem, and #wilsontgh & #Andy responses took me in the right direction. However I couldn't afford removing the needed PanRecognizer from the superview, nor disabling it in certain views.
What worked for me was to set this on the main view recognizer, which avoids cancelling touches handled to other views/recognizers.
panRecognizer.cancelsTouchesInView = NO;
I had this problem too. Check if you have a UIGestureRecognizer (for me it was a UIPanGestureRecognizer) that is attached to the UITableView's superviews. I removed it and the reordering worked.
In my case, it is the "hide nav bar on swipe" feature that caused the re-order to fail. Therefore make sure it is turned off if you need the UITableView re-order feature.
navigationController?.hidesBarsOnSwipe = false
Note that navigation controller is shared across all view controllers of the same navigation stack, therefore one screen can affect all other ones even after pushing/pop-ing operations.
I recently had the same problem and this thread pointed me in the right direction, but the better approach is to use the gesture recognizer's delegate method gestureRecognizerShouldBegin: to determine if you are in a situation where you want to process it or not.
In my situation I had a swipe gesture on a main view controller and was displaying a table in a child view controller. In gestureRecognizerShouldBegin: I return a NO when the child view controller is active, which allows the table to see every gesture and the container view controller doesn't handle anything.
The method tableView:moveRowAtIndexPath:toIndexPath: does get called immediately upon touching the control. From the UITableViewDataSource Protocol Reference:
The UITableView object sends this message to the data source when the user presses the reorder control in fromRow.
So this is expected behavior.
I believe the problem is in your tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath:. Do you have a specific reason to include this method? Have you tried leaving it out and going with the proposed position?
For example, it seems that ([proposedDestinationIndexPath row] < [self.itemArray count]) will always return true...
In my case, the problem was in the keyboardDismissMode property.