My question is about deleting "invisible" rows in a UITableView. When I say invisible, I mean rows that are not showed on the screen. For example, all the rows that aren't returned by calling the UITableView method - (NSArray *)visibleCells.
I'm asking this because I'm developping an "expandable" UITableView. A little bit like a tree. You can have a table like this :
Menu 1
Menu 1.1
Menu 1.2
Menu 2
Menu 2.1
Menu 2.2
And when you click on "Menu 1" the cells "Menu 1.1" and "Menu 1.2" will either appear or disappear. I do this by simply inserting or deleting cells with an animation.
The problem is, if I have long menus and the user scrolls, if half the rows from "Menu 1", for instance, ar hidden (not visible, not showed on the screen, visible only if you scroll down) and that the user wants to reduce "Menu 1" this will cause my application to crash because I'm trying to delete rows that are not visible.
The actual error message is :
* 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 (11) must be equal to the number of
rows contained in that section before the update (15), plus or minus
the number of rows inserted or deleted from that section (0 inserted,
0 deleted) and plus or minus the number of rows moved into or out of
that section (0 moved in, 0 moved out).'
If I do the exact same manipulation with all the rows visible, no problem with the app and the menu behaves correctly.
On your tableview update calls, you need to update the tableview data source as well as the tableview. You can use UITableView's indexPathsForVisibleRows method to find the positions in which to remove the objects from the source, and update your table.
Thanks to all of you for your answers. What I did is I used "indexPathsForVisibleRows" to get the indexPaths of the rows not displayed on the screen and I simply updated my model and called "reloadData" to update my table. This is not the ideal solution I was expecting but it works.
I still don't understand why "-deleteRowsAtIndexPaths" doesn't work for rows not on the screen but actually really present in the table.
I had a similar problem and here goes my solution. Basically the solution involves on changing the cells height to "hide" them.
I've changed the section header to a custom UIControl to looks like any other cell and implemented the follow method to "hide" the rows outside the selected section:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == _selectedSection) {
// If it is the selected section then the cells are "open"
return 60.0;
} else {
// If is not the selected section then "close" the cells
return 0.0;
}
}
For the custom header I've used the following code:
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
CGFloat height = [self tableView:tableView heightForHeaderInSection:section];
// Here you can use whatever you want
CGFloat width = 640.0;
UIControl *view = [[UIControl alloc] initWithFrame:CGRectMake(0.0, 0.0, width, height)];
view.tag = section;
// This code is used by my custom UIControl
//
// to change the style for each state
// view.sectionSelected = (section == _selectedSection);
//
// and to change the title
// view.title = [self tableView:tableView titleForHeaderInSection:section];
// This event is used to "close" or "open" the sections
[view addTarget:self action:#selector(didSelectSectionHeader:) forControlEvents:UIControlEventTouchUpInside];
return view;
}
To be more appealing I've added animations to the following method:
- (void)didSelectSectionHeader:(id)sender
{
if ([sender isKindOfClass:[UIControl class]]) {
// Save the old section index
int oldSelection = _selectedSection;
// Get the new section index
int tag = ((UIControl *)sender).tag;
// Get sections quantity
int numSections = [self numberOfSectionsInTableView:_tableView];
// Check if the user is closing the selected section
if (tag == _selectedSection) {
_selectedSection = -1;
} else {
_selectedSection = tag;
}
// Begin animations
[_tableView beginUpdates];
// Open the new selected section
if (_selectedSection >= 0 && _selectedSection < numSections) {
[_tableView reloadSections:[NSIndexSet indexSetWithIndex:_selectedSection] withRowAnimation:UITableViewRowAnimationAutomatic];
}
// Close the old selected section
if (oldSelection >= 0 && oldSelection < numSections) {
[_tableView reloadSections:[NSIndexSet indexSetWithIndex:oldSelection] withRowAnimation:UITableViewRowAnimationAutomatic];
}
[_tableView endUpdates];
}
}
Related
I've got a project (that has been written by other people) where there's a feed with content and text displayed inside a table view. Each post corresponds to one section in table view, and each section has its own rows corresponding to elements like content, text, like button etc.
I need to display a short label for post captions with a "more" button inside table view cell, and when more button is tapped, the label will expand to whatever size the caption fits, all happening inside a table view cell. When the more button is tapped I change the label's numberOfLines property to zero, and as the cells have automatic height, all I need is to reload that particular caption cell. (the cell displays correctly with the expanded size if I set numberOfLines to 0 at the first place before displaying the cell.)
I've tried:
[tableView beginUpdates];
tableView endUpdates];
I've tried various animation options with:
[tableView reloadRowsAtIndexPaths:#[myPath] withRowAnimation:UITableViewRowAnimation(Bottom,Top,None etc)];
I've tried:
[tableView reloadSections:[NSIndexSet indexSetWithIndex:myPath.section] withRowAnimation:UITableViewRowAnimation(Top,Bottom,None etc)];
But they all yield the same result: the whole table view layout gets messed up: it jumps to another cell, some views go blank, cells overlap each other, video inside cells stop playing, and the label doesn't expand (but refreshes inside itself, e.g. that short preview txt with one line animates from top/bottom etc but doesn't expand).
What might be causing the mess up of the whole table view and how I can reload just one cell correctly with and expansion animation, without messing up the whole layout? I've seen many questions and answers regarding this, but they all recommend the options that I've already tried and explained above.
My app targets iOS 8.0+
UPDATE: Here is the relevant code (with some parts regarding inner workings that aren't related to layout, removed):
MyCell *cell = (MyCell *)[tableView dequeueReusableCellWithIdentifier: MyCellIdentifier forIndexPath:indexPath];
cell.delegate = self;
cell.indexPathToReloadOnAnimation = indexPath;
cell.shouldShortenCaption = YES;
id post = self.posts[indexPath.section] ;
[cell setPost:post];
return cell;
And inside setPost::
if(self.shouldShortenCaption){
captionLabel.numberOfLines = 2;
}else{
captionLabel.numberOfLines = 0;
}
NSString *text = [some calculated (deterministic) text from post object];
Button action is simple:
self.shouldShortenCaption = NO;
[one of the reload codes that I've written above in the question]
UPDATE 2: Here are some more methods regarding the issue:
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
if (section < self.posts.count) {
return 59;
}else
return 0;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
if (section < self.posts.count) {
MyFeedHeader *header = [tableView dequeueReusableCellWithIdentifier:MyFeedHeaderIdentifier];
header.delegate = self;
[header setPostIndex:section];
[header setPost:self.posts[section]] ;
return header;
}
else
return nil;
}
Header's setPost: method basically sets the relevant texts to labels (which have nothing to do with the caption, they are completely different cells. the problematic cell is not the header cell). The table doesn't have any footer methods. The only method regarding height is the one above.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (section >= self.posts.count) {
return 1;
}
id post = self.posts[section];
[calculate number of rows, which is deterministic]
return [number of rows];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
BOOL hasCursor = self.cursor && self.hasMore ? 1 : 0;
return self.posts.count + hasCursor;
}
(Post count and cursor and hasMore are also deterministic).
UPDATE 3: I've asked a question (which wasn't a duplicate, even though there are similar questions) and got a useful answer that solved my problem. Can the downvoters please elaborate the reason that they've downvoted?
Here is an example: https://github.com/DonMag/DynamicCellHeight
Table B is one way of accomplishing "More/Less" (Table A was for another layout I played around with). It uses the [tableView beginUpdates]; tableView endUpdates]; method of triggering the table re-layout.
The key is getting all your constraints set up correctly, so the Auto-Layout engine does what you expect.
The example is in Swift, but should be really easily translated back to Obj-C (I think I did it in Obj-C first).
Edit: some additional notes...
This is using a pretty standard method of dynamic-height table view cells. The vertical spacing constraints between elements effectively "pushes out" the bounds of the cell. The tap here toggles the numberOfLines property of the label between 2 and 0 (zero meaning as many lines as necessary). Sequential calls to beginUpdates / endUpdates on the table view tells the auto-layout engine to recalculate the row heights without needing to reload the data.
For this example, I did use a little "trickery" to get the smooth expand/collapse effect... The multiline label you see here is contained in a UIView (with clipsToBounds true). There is a second, duplicate multiline label (alpha 0 so it's not visible) that is controlling the height. I found that changing the numberOfLines on the visible label sort of "snapped" to 2 lines, and then the size change animation took place... resulting in the text "jumping around."
Just for the heck of it, I added the "not so good" version to my GitHub repo for comparison's sake.
I have a table view with (ex : 20 rows). and I have used a custom table view cell with this table view.
inside this table view cell, there are few labels, one button and a hidden view (UIView).
I have written button action for hide/show the hidden view inside the custom table view cell class.it works fine.but it affect to other rows in the table view. that means, when I tap the button in first row, then the hidden view show, and it can see in some other rows in the table view when scroll down.
At the same time (when hide/show), I want to increase and decrease the row height (only clicked row/cell) . what is going wrong. below is my codes and some screen shots to get an idea.
note : cell expand/increase it self when click on expand button in each cell.
this is how I hide and show the hidden view, inside the custom table view cell class.
- (IBAction)hideshow:(id)sender {
BOOL ishidden = self.insideCollectionView.hidden;
if(ishidden == true)
{
self.insideCollectionView.hidden = false;
}
else
{
self.insideCollectionView.hidden = true;
}
}
what is going wrong, hope your help with this.
Advance : it is great if there is a way to do both hide/show and expand(increase the row height) of the cell when click on expand button for each cell.
Found the suited solution for this by my self. thanx everyone who supported me.
Do the followings.
Create a NSMutableArray to hold Which button in Which row
clicked.
Then when user click on the Button in Custom table view cell, check it that index path is already in the mutable array, if it is already inside it, then romove it ,otherwise add it.
Then in the cellforrowatindexpath method, check that nsmutable array and , check whether the indexpath.row is exist or not.
Finally, if it is exists, do not hide, else hide it, this works perfectly.
here is the implementation for the table view. .m file
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 25;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
FirstTableViewCell *cells = [tableView dequeueReusableCellWithIdentifier:#"tvcell" forIndexPath:indexPath];
NSString *theIndexpath = [NSString stringWithFormat:#"%ld", (long)indexPath.row];
//here check whether it is exists or not.
if ([chekcme containsObject:theIndexpath])
{
cells.insideCollectionView.hidden = false;
}
else
{
cells.insideCollectionView.hidden = true;
}
[cells setColletionData:bbarray];
[cells.expandMe addTarget:self action:#selector(runs:) forControlEvents:UIControlEventTouchUpInside];
//in here set the tag of the button to indexpath.row
cells.expandMe.tag = indexPath.row;
return cells;
}
//this is the action for the button inside the custom tableview cell.
- (IBAction)runs:(UIButton *)sender{
NSString *myob = [NSString stringWithFormat:#"%li", (long)sender.tag];
NSLog(#"%#",myob);
if ([chekcme containsObject:myob]) {
[chekcme removeObject:myob];
}
else
{
[chekcme addObject:myob];
}
NSLog(#"%#", chekcme);
//keep in mind to reload the table view here.
[self.maintableView reloadData];
}
note : checkme is NSMutableArray to holds the objects clicked by the user.
I will need to see the code in your tableview data source methods, but i think following can solve your issues:
Issue #1: I am assuming your are using deque for your cell, this is causing the cell to be reused when you scroll. I will suggest you to maintain the state of your each cell (eg: isExpanded) and configure cell accordingly in cellForRowAtIndex:
Issue #2: Use the same 'IsExpanded' in heightForRowAtIndex: and call reloadRowsAtIndexPaths: on the table for your cell when state is changed for 'IsExpanded'
That the button affects several rows is because you store the BOOL in the collectionView in the cell.
When the cell is reused for another row, that cell will be hidden/shown based on the status of the row it was previously used for. As has already been suggested you need to store this state for each and every one of your rows and set it accordingly when the cell is prepared by the dataSource.
You should set the status about the view initially in cellForRowAtIndexPath. Hope it will help you.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// here you have to maintain the status about the current cell. whether it is hidden or not.
cell.insideCollectionView.hidden = status;
}
I,am doing the same thing like:
In didSelect
switch selectedIndexPath {
case nil:
selectedIndexPath = indexPath
default:
if selectedIndexPath! == indexPath {
selectedIndexPath = nil
} else {
selectedIndexPath = indexPath
}
}
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)
And at row height
if selectedIndexPath != nil {
if selectedIndexPath == indexPath {
return estimatedHeight
}
}
return 44
The selectedIndexPath is a property of NSIndexPath.
Hope this may help you.
you have need to reload all table instead of single cell
you can use below code
tbl_name .beginUpdates(
tbl_name .endUpdates()
Update
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
if indexPath.row == userselectedrow // save selectecd cell index if you want to only one cell expand otherwise save selected row in array and compare to here
{
return incrementheight // increment row height here
}
else
{
return decrementheight // decrement row height
}
}
- (IBAction)hideshow:(UIButton*)sender {
CGPoint point11=[sender convertPoint:CGPointZero toView:tbl_calender];
NSIndexPath index_pathGlobal =[tbl_calender indexPathForRowAtPoint:point11]; // save index_pathGlobal
}
hope this will be help you
Step 1:- Assume that you are showing data in rows like label value, button and hidden view.Add one parameter in your array ex. isViewNeedToShow, By default fill that value FALSE.
Step 2:- After that in your button action your are passing indexPath.row as tag value in cell for row at index path, So on button action change the array vale parameter isViewNeedToShow == TRUE, and reload the section of table view. For ex:-
Item *tempItem = yourArray[sender.tag];
tempItem. isViewNeedToShow = tempItem. isViewNeedToShow ? FALSE : TRUE;
**For particular row update** :-
[yourTableView beginUpdates];
[yourTableView reloadRowsAtIndexPaths:#[sender.tag, 0] withRowAnimation:UITableViewRowAnimationNone];
[yourTableView endUpdates];
Step 3:- If you want to expand table cell you have to calculate height of row that contains the items like view, label etc.
Or if you are using auto layouts in your project use UI Table View Delegate Methods
-(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
I hopes your problem would be resolved.
I am making an application where I am getting a user's phone book and displaying it in a list view. I am breaking the phone book into sections sorted by their name. I want to alternate rows by even and odds HOWEVER the problem is the indexPath.row resets back to zero if a new section starts. I am trying to get whetehr it is an odd or even row AGNOSTIC of the section it is in
i tried
- (void)tableView:(UITableView *)tableView
willDisplayCell:(UITableViewCell *)cell
forRowAtIndexPath:(NSIndexPath *)indexPath
{
if(switcher == 0)
{
switcher = 1;
}
else
{
switcher = 0;
}
}
hoping that the odds even selector would change every time a new cell will be displayed.
THIS ALMOST works however sometimes if i stop and start scrolling again I WILL GET zero twice or 1 twice (it is not perfect).
Thank you in advance!
[I added a picture for the current result and another for the desired result
Add the number of rows that come in all the sections before the given section. Then you can do a modulus operation to determine if it is even or odd. You can use this in cellForRowAtIndexPath: to change the color based on isOdd.
NSUInteger row = indexPath.row;
for (int i = 0; i < indexPath.section; i++) {
row += [tableView numberOfRowsInSection:i];
}
BOOL isOdd = row % 2; // if it is divisible by 2, this will be 0. if it isn't, it is 1
I have a UITableView with two sections. Items can move from one section to another, as seen in the image below.
The basic interaction is as follows:
you swipe a cell/row
that cell is removed from its section
that cell is added to the bottom of the other section
Note, I'm using a sequence of deleteRowsAtIndexPaths and insertRowsAtIndexPaths with CATransactions. Remove followed by add isn't the only table operation in place; for the general approach I want chained insert/delete/update animations to start when the preceding operation has finished. Here's an example:
[CATransaction begin];
[CATransaction setCompletionBlock:^{
[table beginUpdates];
// update data source...
// insertRows...
[table endUpdates];
}];
[table beginUpdates];
// update data source...
// deleteRows...
[table endUpdates];
[CATransaction commit];
When there are enough items in the table view to take up more than a screen's worth of space, (what I think is) the default UITableView scrolling kicks in when cells are removed from one section and added to another.
My assumption (as I haven't found any reference to scroll behaviour in the docs) is that the UITableView scrolls to place/keep the inserted row in view.
Consider the example of swiping a cell to remove it from one section and have it added to the other:
the cell is currently in view because the user is swiping it (so perhaps there is no change/scroll required for the deleteRows...
(in some circumstances) the table view scrolls for the subsequent insertRows...; the scroll happens before the insert so that the animated insert is observed
For example, in the image below, swipe one of the cells in the bottom section, table view scrolls, then inserts at the bottom of the top section...
I say some circumstances because this behaviour is observed if the insert takes place in the top section (section 0). When the delete / insert is from section 0 to section 1, the table view does not scroll at all.
I'm also seeing some other behaviour I don't understand. In the case where some of the cells have more than one line of text (cells aren't all the same size) - the scroll seems to "not work". At first the scroll amount simply appeared wrong; however, testing with all the cells being single line revealed consistent scrolling when the insert occurred in section 0.
The following image shows the wrong scrolling:
I have two specific questions and one general question.
Specific question #1: is there a hierarchy for UITableViews such that section 0 is preferred, so the table view will scroll to section 0 so that inserts are observed, but not scroll to other sections?
Specific question #2: is it the height of the cells in the above example that is causing the wrong scrolling to be observed?
This is the code I'm using for calculating the height of the table view cells:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger index = indexPath.row;
ParsedItem* item;
if (indexPath.section == 0) {
item = [_parsedItemList.toDoList objectAtIndex:index];
}
else {
item = [_parsedItemList.doneList objectAtIndex:index];
}
NSAttributedString* attrString = [[NSAttributedString alloc] initWithString:item.displayText attributes:_tableViewTextAttrNormal];
CGFloat height = [self textViewHeightForAttributedText:attrString andWidth:300.00];
return fmaxf(50.0, height);
}
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return 50.0;
}
- (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andWidth:(CGFloat)width {
UITextView* calculationView = [[UITextView alloc] init];
[calculationView setAttributedText:text];
CGSize size = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];
return size.height;
}
General question: as I mentioned above, I haven't found anything in the docs that describes how the scrolling is supposed to work. Is there a general rule or principal that I've missed? I don't want to add explicit scroll operations if I'm simply doing it wrong...
I'm creating a list app where the user can check off items, the checked items move to the bottom (a different section in the UITableView) when checked after a short delay. The user can "uncheck" the item by tapping it and it moves back to the original section.
My problem is that when the cells are moving between sections like this it seems the UITableView get "confused" about where which cells are.
When I NSLog the IndexPath.row of the cell I'm interacting with it returns a different row than I'm touching. I use [self.tableView moveRowAtIndexPath:from_indexPath toIndexPath:to_indexPath]; to move the cells between sections. Here's some of my code:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
switch (indexPath.section) {
case SECTION_ACTIVE:{
ItemData *dataForCell = [activeList objectAtIndex:indexPath.row];
NSLog(#"%# \t %d",dataForCell.title, indexPath.row);
if (dataForCell.isHeader) {
NSLog(#"%d",indexPath.row);
return 70;
}
break;
}
default:
break;
}
return 55;
}
Is there any way to work around this? Or to fix this?
You can tag your table cells and can re-check it when you move your cell from one section to another.