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.
Related
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.
I'm trying to implement the swipe left to display the delete button.
The cell moves left, but there are no visible buttons. Why does this happen? What is the solution?
To be clear, I have searched on StackOverflow and tried at least a dozen posted "solutions", but none of them have worked for me.
Thanks in advance.
I tried using the following code, which does seem to make the table cell move, but the log does not show NSLog(#"commitEditingStyle");. Why does this happen?
// Swipe to delete.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"commitEditingStyle");
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSLog(#"UITableViewCellEditingStyleDelete");
[listData removeObjectAtIndex:indexPath.row];
[self.tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView reloadData];
}
}
And I have tried adding:
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
Here is a screenshot:
The problem was that the table size was larger than my screen size. The solution is to set the constraints to match the width of the view.
// During startup (-viewDidLoad or in storyboard) do:
self.tableView.allowsMultipleSelectionDuringEditing = NO;
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
// Return YES if you want the specified item to be editable.
return YES;
}
// Override to support editing the table view.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
//add code here for when you hit delete
}
}
When an user deletes some table row with swipe-to-delete action,
Instruments Tool shows that the deleted UITableViewCell instance is still alive.
I used very ordinary approach that is:
-(UITableViewCellEditingStyle) tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
return UITableViewCellEditingStyleDelete;
}
-(void) tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if(editingStyle == UITableViewCellEditingStyleDelete){
// Do Some Processing Model things...
[self.tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation: UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
}
}
I can't sure but I think It is bug that is related with ARC.
May I leave this problem or should I have to find any walk around?
TableView holds on to the cell so it can reuse them later.
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 have a table view with custom cells. The cells are filled with my data.
Now I want to enable the user to rearrange the rows. I have implemented the methods, but while dragging to reorder the cell, I can see it shows like it is trying to do but cannot move anywhere. It moves like 10 pixel as if it will do the rearrange but goes back to its position. How to reorder the rows with custom cell?
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
[self.dataSource removeObjectAtIndex:indexPath.row];
[tableView reloadData];
}
}
-(UITableViewCellEditingStyle)tableView:(UITableView*)tableView editingStyleForRowAtIndexPath:(NSIndexPath*)indexPath
{
if (self.mytableView.editing)
{
return UITableViewCellEditingStyleDelete;
}
return UITableViewCellEditingStyleNone;
}
-(BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
-(BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
-(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
id stringToMove = [self.dataSource objectAtIndex:sourceIndexPath.row];
[self.dataSource removeObjectAtIndex:sourceIndexPath.row];
[self.dataSource insertObject:stringToMove atIndex:destinationIndexPath.row];
}
-(NSIndexPath *)tableView:(UITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath
{
if (proposedDestinationIndexPath.section != sourceIndexPath.section)
{
return sourceIndexPath;
}
return proposedDestinationIndexPath;
}
I know this is old but I will still answer it. The issue here is with your tableView: targetIndexPathForMoveFromRowAtIndexPath: toProposedIndexPath: method (your last method)
Your logic is preventing any moving from happening. Your if-statement:
if (proposedDestinationIndexPath.section != sourceIndexPath.section)
is saying if the desired location (the location the user wants to bring the cell) is not my current location, then return my current location (so do not move the cell). Otherwise, if my desired location (the new location I want to go) is my current location then return the desired location (which is actually my current location).
I hope that makes sense, so basically you are saying that no matter what, make sure every cell always remains in its current location. To fix this, either remove this method (this is not needed unless there are moves that are illegal) or switch your two return statements, so:
-(NSIndexPath *)tableView:(UITableView *)tableView
targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath
toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath {
if (proposedDestinationIndexPath.section != sourceIndexPath.section) {
return proposedDestinationIndexPath;
}
return sourceIndexPath;
}
In fact, the only method required to allow re-arranging is: tableView: moveRowAtIndexPath: toIndexPath:. So again, unless you want specific behavior out of the other methods you can save some code and just remove most of them (especially since in this situation you are mainly just implementing the defaults).