Does the model change and the row animation have to be started directly from commitEditingStyle or can it be done in a later run loop iteration?
I am asking because it appears to work on iOS 10, but on iOS 11 it breaks at least the delete animation. Is it simply a bug in iOS 11 or is it a bad idea in general?
Is there a better way to trigger an asynchronous delete operation and animate the table view change on completion?
The first picture shows how it breaks on iOS 11 (The delete button overlaps the next cell). The second picture shows how it looks fine on iOS 10.
This is the interesting snipped:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
dispatch_async(dispatch_get_main_queue(), ^{
[_model removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
});
}
}
If I remove the dispatch_async(..., it works as expected on iOS 10 and 11. The first picture shows iOS 11, the second iOS 10.
Here is the full code of the table view controller used to test it:
#import "TableViewController.h"
#implementation TableViewController {
NSMutableArray<NSString *>* _model;
}
- (void)viewDidLoad {
[super viewDidLoad];
_model = [[NSMutableArray alloc] init];
[self.tableView registerClass:UITableViewCell.class forCellReuseIdentifier:#"cell"];
for (NSInteger i = 0; i < 200; i++) {
[_model addObject:[NSString stringWithFormat:#"Test Row %ld Test Test Test Test Test", (long)i]];
}
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _model.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell" forIndexPath:indexPath];
cell.textLabel.text = _model[indexPath.row];
return cell;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
dispatch_async(dispatch_get_main_queue(), ^{
[_model removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
});
}
}
#end
Update:
Adding this method to the table view controller fixes it for iOS 11 and allows delaying the model change and row animation. (Thanks to ChrisHaze)
- (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(11_0) {
UIContextualAction* deleteAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:#"Delete" handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
dispatch_async(dispatch_get_main_queue(), ^{
[_model removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
completionHandler(YES);
});
}];
UISwipeActionsConfiguration* config = [UISwipeActionsConfiguration configurationWithActions:#[deleteAction]];
return config;
}
It seems that there have been changes made to the UITableview in iOS 11.
Apple's Build release notes :
"The behavior of the delete swipe action has been changed.When implementing commitEditingStyle: to delete a swiped row, delete the row in the data source and call deleteRowsAtIndexPaths: on the table view to show the swipe delete animation."
After further research, I found that you must call the beginUpdates prior to calling the deleteRowAtIndexPath: method, along with the endUpdates method after.
According to Apple's documentation, not calling these tableView methods will result in potential data model issues and effect the deletion animations.
With that said, there is an answer that includes the required code to a question on the Apple dev forums that addresses a very similar question.
Answering your actual questions:
Model changes need to be called prior to the beginUpdates and endUpdates block, in which the UI changes will occur.
Updating the UITableView will alleviate the synchronization/animation issues.
---------------------------- additional details ----------------------------
After looking into the details within your comment, I've included a link (above) that is the latest Table View Programming Guide provided by Apple. It will take you, or anyone else with this issue, to the section of the guide that includes the details you've added to your question.
There is also a handy note that includes how to make the deleteRowAtIndexPath call from within the commitEditingStyle method if one must.
Related
I have an application with a UITableView. The application communicates with a server.
Problem is the following:
Client 1 deletes a cell of the TableView. The data update is transmitted to the server which sends a data update to Client 2. In the same moment Client 2 deletes a cell from the TableView. Due to the receiving update an NSInternalInconsistencyException is thrown because the number of cells before and after are not as expected (difference is 2 not 1).
The app crashes in
[tableView endUpdates];
Of course I can catch the exception with
- (void) tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
#try{
// Send data update to the server
[sender sendDataToServer:data];
// Update tableview
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
}
#catch (NSException * e) {
// Hide red delete-cell-button
tableView.editing = NO;
// Jump to previous ViewController
[self.navigationController popViewControllerAnimated:YES];
}
}
}
But after the successful catch I get an
EXC_BAD_ACCESS exception in [_UITableViewUpdateSupport dealloc].
What can I do to prevent the app from crashing? What do I have to do in the catch-block to "clean" the situation? Reloading the tableview doesn't take any effect.
EDIT: numberOfRows-method looks like this:
- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [dataSourceArray count];
}
#synchronized(lock) solved my problem.
I just had to make sure that only one thread is able to manipulate the data source array at the same time. Now everything works fine, no more 'NSInternalInconsistencyException'.
Thx to trojanfoe and luk2302.
I'm experiencing pretty mysterious glitch:
After endUpdates was called all removed updated cells becoming just hidden and new cell will created, without reusing existing ones.
Usually even -tableView:didEndDisplayingCell:forRowAtIndexPath: not called, but not always.
Result looks like this:
I have no any clue about reasons of this behavior and will appreciate any ideas how to debug and fix this.
Unfortunately, source code it pretty complex and I'm unable to extract something intergal. if you ask me about related code - i will copy it to there.
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
id item = [self.presenter.mediator itemAtIndexPath:indexPath];
NSString *reuseIdentifier = [self.reuseIdentifierMatcher reuseIdentifierForItem:item];
UITableViewCell<ViewItemProtocol>* cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier
forIndexPath:indexPath];
cell.item = item;
return cell;
}
- (void)applyChangesSet:(TableChangesSet*)changes
{
LOG_VALUE(#([[NSThread currentThread] isMainThread]));
[self.tableView beginUpdates];
for (TableChange *change in changes.changes)
{
[self applyChange:change];
}
[self.tableView endUpdates];
}
applyChange does not contains any UI manipulations, except deleteRowsAtIndexPaths/insertRowsAtIndexPaths/reloadRowsAtIndexPaths;
Debug output is #([[NSThread currentThread] isMainThread]) is 1
I prepared a TableView containing data from NSMutableArray. There is an option to edit and reorder rows. All works fine, until the phone is shut down or too many applications are running in the background - there is an error message in Xcode that the application exited unexpectedly due to memory pressure.
I would like to add some command to remain and remember previous cell order so after iPhone shut down user will have the same order as before.
Below is the code I am using for enabling reorder. Is there anything missing? Please bear with me, these are just my initial developing attempts.
Thanks in advance for any help!
Matus
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath: (NSIndexPath *)indexPath {
return UITableViewCellEditingStyleDelete;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle: (UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
[_Title removeObjectAtIndex:indexPath.row];
[_Images removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
NSString *item = [self.Title objectAtIndex:fromIndexPath.row];
[self.Title removeObject:item];
[self.Title insertObject:item atIndex:toIndexPath.row];
NSString *image = [self.Images objectAtIndex:fromIndexPath.row];
[self.Images removeObject:image];
[self.Images insertObject:image atIndex:toIndexPath.row];
}
- (void)tableView:(UITableView *)tableView didEndReorderingRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:indexPath.section]
withRowAnimation:UITableViewRowAnimationNone];
}
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
I really wish I could comment at this point... So, I already apologize for writing this as an answer.
You really shouldn't discard the fact that your app is being shut down by the OS. This is the main issue. Not the persistence of the data.
After you fix that, you can then look into how to persist your data (using Core Data, or a simple plist file) when leaving the app (using either one of the method in the UIApplicationDelegate protocol, or one of the application notifications). And loading it back when reopening it.
You need to make sure that you're persisting the changes to self.Title and self.Images.
If you add a call to the code you use to save the order of your rows at the end of tableView:MoveRowAtIndexPath:toIndexPath: then any change in the order of the rows should always be saved.
You can test that this is working by stopping the app (either by Cmd+. or the stop sign) immediately after you move a row.
I took approach of the UITableview to get cell click ExpandView. I want something like this to be implement.
So, UITableView would be best approach for this or suggest me any good way of implementing, also I am not able to get subview to adjust according to screenSize.
Could be there are another ways to accomplish this but this is how I am expand UITableViewCell on the fly. This could give the idea and you can implement your solution.
I keep row heights in my data model:
- (void)viewDidLoad {
[super viewDidLoad];
//Data source
datasource = [[NSMutableArray alloc] init];
NSMutableDictionary *aDicti = [[NSMutableDictionary alloc] init];
[aDicti setValue:#"a TEXT" forKey:#"text"];
[aDicti setValue:#(50) forKey:#"cellheight"]; // here
}
When selection changed, just updating related key in data source.
-(void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView beginUpdates];
[[datasource objectAtIndex:indexPath.row] setObject:#(50) forKey:#"cellheight"];
[tableView endUpdates];
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView beginUpdates];
[[datasource objectAtIndex:indexPath.row] setObject:#(200) forKey:#"cellheight"];
[tableView endUpdates];
}
Once [tableView endUpdates]; executed heightForRowAtIndexPath and numberOfRowsInSection delegate methods fired and automatically adjust cell height with the value from data source.
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [[[datasource objectAtIndex:indexPath.row] objectForKey:#"cellheight"] intValue];
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return datasource.count;
}
If you do not want to keep row height in your data source you can basically apply this.
-(void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView beginUpdates];
[tableView endUpdates];
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[tableView beginUpdates];
[tableView endUpdates];
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (tableView.indexPathForSelectedRow.row == indexPath.row) {
return 100;
}
return 50;
}
That's call, accordion, this site having good examples (with demos) for it, here's the link.
Basically when you are working with TableView you should play with number of section headers and cells.
In case of using section headers you should set numberOfRowsInSection to 0 (roll up) to X when you want to expand it. After that call
tableView.reloadData
I implemented this behavior here, with animation, with different heights:
https://github.com/rudald/expandedTableViewIOSSwift/
There are numerous open source projects regarding this feature. I've downloaded a few projects and the most customizable and issue-less, for me, was SLExpandableTableView
SDNestedTable does exactly what you want.
The module concept is that of having all the default functionality of
a UITableView and its cells while at the same time adding for each
cell a child UITableView. Each cell (SDGroupCell) in the main
SDNestedTableViewController tableview acts as controller for its own
sub table. The state, population and behavior of the table and
subtable is instead mostly controlled by
SDNestedTableViewController.
If you’re looking for a straight forward easy-to-setup library for expandable views, HVTableView is your choice. It provides an acceptable performance which is sufficient for using in regular projects.
I´m quite new to iOS development and I´m having a terrible time by trying something that should be easy; to add an extra row in a TableView everytime the user clicks on one of the existing rows. There is no real purpose on that action, I´m just wanting to understand the behaviour of TableView.
So I did the following:
I used a Split View-based template and changed the number of rows to 30 in the RootViewController.
- (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section {
// Return the number of rows in the section.
return 30;
}
The method tableView:didSelectRowAtIndexPath looks in the following manner:
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
/*
When a row is selected, set the detail view controller's detail item to the item associated with the selected row.
*/
NSMutableArray* paths = [[NSMutableArray alloc] init];
NSIndexPath *indice = [NSIndexPath indexPathForRow:30 inSection:0];
[paths addObject:indice];
detailViewController.detailItem = [NSString stringWithFormat:#"Second Story Element %d with all its information and bla bla bla", indexPath.row];
[[self tableView] beginUpdates];
[self.tableView insertRowsAtIndexPaths:(NSArray *) paths withRowAnimation:UITableViewRowAnimationNone];
[[self tableView] endUpdates];
}
When I execute the program and click on one of the elements, I receive 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 (30) must be equal to the number of rows contained in that section before the update (30), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted).'
I did not change any other part of the code that the template provides.
I read quite extensively the documentation from Apple and the responses to the following questions:
Add a row dynamically in TableView of iphone
and
how to properly use insertRowsAtIndexPaths?
The second question seems to address the same problem, but I´m not capable to understand what is happening. What do they mean with dataSource? The response that I understand better says the following:
It's a two step process:
First update your data source so numberOfRowsInSection and cellForRowAtIndexPath will return the correct values for your post-insert data. You must do this before you insert or delete rows or you will see the "invalid number of rows" error that you're getting.
What does this update of the data source implies?
Sample code would be HIGHLY appreciated, because I´m totally frustrated.
By the way, all that I´m trying has nothing to do with entering the editing mode, has it?
You need to keep the count returned by tableView:numberOfRowsInSection: in sync!
So when you have 30 rows and then tell the tableview to insert a new row you need to make sure tableView:numberOfRowsInSection: will now return 31.
- (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section
{
return self.rowCount;
}
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
self.rowCount++;
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:(NSArray *) paths withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
}
In practice you would probably use an array to track your rows return [self.rows count]; etc
The answer is quite simple. When you want to modify a table view you need to perform two simple steps:
Deal with the model
Deal with table animation
You already perform the second step. But you have missed the first one. Usually when you deal with a table you pass it a data source. In other words some data to display within it.
A simple example is using a NSMutableArray (it's dynamic as the name suggests) that contains dummy data.
For example, create a property like the following in .h
#property (nonatomic, strong) NSMutableArray* myDataSource;
and in .m synthesize it as:
#synthesize myDataSource;
Now, you can alloc-init that array and populate it as the following (for example in viewDidLoad method of your controller).
self.myDataSource = [[NSMutableArray alloc] init];
[self.myDataSource addObject:#"First"];
[self.myDataSource addObject:#"Second"];
Then, instead of hardcoding the number of rows you will display (30 in your case), you can do the following:
- (NSInteger)tableView:(UITableView *)aTableView numberOfRowsInSection:(NSInteger)section {
return [self.myDataSource count];
}
Now, in you didSelectRowAtIndexPath delegate you can add a third element.
- (void)tableView:(UITableView *)aTableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self.myDataSource addObject:#"Third"];
[[self tableView] beginUpdates];
[self.tableView insertRowsAtIndexPaths:(NSArray *) paths withRowAnimation:UITableViewRowAnimationNone];
[[self tableView] endUpdates];
}
It looks like one big problem is with tableView:numberOfRowsInSection:. You need to return the correct number of rows in that method.
To do that, it's usually best to maintain an NSArray or NSMutableArray of items for the table view so in that function, you can say: return [arrayOfValues count];. Keep the array as a property of your view controller class so that it's readily accessible in all methods.
The array can also be used in cellForRowAtIndexPath:. If you have an array of NSString, you can say cell.text = [arrayOfValues objectAtRow:indexPath.row];.
Then, when you want to add an item to the table view, you can just add it to the array and reload the table, e.g. [tableView reloadData];.
Try implementing this concept and let me know how it goes.
You can Also do that for dayanamic table cell
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [arrayStationStore count];
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIndentyfire;
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIndentyfire];
if (cell == nil)
{
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIndentyfire];
}
cell.textLabel.text = [arrayStationStore objectAtIndex:indexPath.row];
return cell;
}
-(NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// Check if current row is selected
BOOL isSelected = NO;
if([tblStationName cellForRowAtIndexPath:indexPath].accessoryType == UITableViewCellAccessoryCheckmark)
{
isSelected = YES;
}
if(isSelected)
{
[tblStationName cellForRowAtIndexPath:indexPath].accessoryType = UITableViewCellAccessoryNone;
[arrayReplace removeObject:indexPath];
NSLog(#"array replace remove is %# ",arrayReplace);
}
else
{
[tblStationName cellForRowAtIndexPath:indexPath].accessoryType = UITableViewCellAccessoryCheckmark;
[arrayReplace addObject:indexPath];
NSLog(#"array replace add is %# ",arrayReplace);
}
return indexPath;
}