28

I have a UITableView, where there is a UISegmentedControl in the header view. It should work exactly like in the App Store app: As the user scrolls, the title in the header scrolls off the screen but the segmentedControl sticks under the navigationBar.

screen

When the user selects a segment, the section below the header should be reloaded with a nice UITableViewRowAnimation. However, as I call tableView:reloadSections:withRowAnimation:, the header view is animated as well, which I want to prevent, because it looks terrible.

Here's my code for this:

- (void)selectedSegmentIndexChanged:(UISegmentedControl *)sender
{
    int index = sender.selectedSegmentIndex;
    if (index < self.oldIndex) {
        [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationLeft];
    } else if (index > self.oldIndex) {
        [self.tableView reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationRight];
    }
    self.oldIndex = index;
}

Anyone has an idea how to reload the section below the header without reloading the header itself?

  • maybe you would just reload the rows only... – holex Dec 27 '13 at 14:53
  • You're right, but do you have a solution that doesn't crash when the amount of cells after the reload is different to the amount before? –  Dec 27 '13 at 16:04
  • 1
    yes, there is a proper solution for it, it called `–deleteRowsAtIndexPaths:withRowAnimation:` and `–insertRowsAtIndexPaths:withRowAnimation:`. you will find more information about it in your personal _Bible_ from Apple: https://developer.apple.com/Library/ios/documentation/UIKit/Reference/UITableView_Class/Reference/Reference.html – holex Dec 27 '13 at 23:14
  • Unfortunately, I get the same error here: `*** Assertion failure in -[UITableView _endCellAnimationsWithContext:]`... –  Dec 28 '13 at 10:19
  • there are many good examples out there of how you can do it properly; or if you read the documentation I've linked for you, that will also help you to achieve what you'd like. working with `UITableView`s is one of the most complex part of the iOS development, I won't blame you if you find to deal with them hard. – holex Dec 28 '13 at 12:12
  • @boeqqsh Have you found any solution? – Hamid Aug 13 '14 at 03:10
  • @Hamid No, I left out the animation and called `[self.tableView reloadData];` This works quite well. –  Aug 14 '14 at 10:49
  • Refer to [Reload tableView section without reloading header section - Swift](https://stackoverflow.com/a/50383024/6521116) – LF00 May 17 '18 at 03:48

7 Answers7

27

Maybe you should try with

[self.tableView reloadRowsAtIndexPaths:[self.tableView indexPathsForVisibleRows] withRowAnimation:UITableViewRowAnimationLeft] //or UITableViewRowAnimationRight

However, I'm not sure but I think it can rise some error in the case where you have less rows to reload than previously.


Edit

I think you could deal with [tableView beginUpdates] and [tableView endUpdates] to solve your problem.

For example, you have 2 arrays of data to display. Let name them oldArray and newArray. A sample of how what you could do :

- (void)selectedSegmentIndexChanged:(UISegmentedControl *)sender
{
    [self.tableView setDataSource: newArray];
    int nbRowToDelete = [oldArray count];
    int nbRowToInsert = [newArray count];

    NSMutableArray *indexPathsToInsert = [[NSMutableArray alloc] init];
    for (NSInteger i = 0; i < nbRowToInsert; i++) {
        [indexPathsToInsert addObject:[NSIndexPath indexPathForRow:i inSection:section]];
    }

    NSMutableArray *indexPathsToDelete = [[NSMutableArray alloc] init];
    for (NSInteger i = 0; i < nbRowToDelete; i++) {
        [indexPathsToDelete addObject:[NSIndexPath indexPathForRow:i inSection:section]];
    }

    [self.tableView beginUpdates];
    [self.tableView deleteRowsAtIndexPaths:indexPathsToDelete withRowAnimation:UITableViewRowAnimationLeft];
    [self.tableView insertRowsAtIndexPaths:indexPathsToInsert withRowAnimation:UITableViewRowAnimationRight];
    [self.tableView endUpdates];
}
Maen
  • 10,603
  • 3
  • 45
  • 71
zbMax
  • 2,756
  • 3
  • 21
  • 42
  • I tried this of course, but as you say, it produces an error if the amount of cells is different:`*** Assertion failure in -[UITableView _endCellAnimationsWithContext:]` –  Dec 27 '13 at 15:58
4

If you are using Swift 2.0, feel free to use this extension.

Be warned: passing in the wrong oldCount or newCount will crash you program.

extension UITableView{

func reloadRowsInSection(section: Int, oldCount:Int, newCount: Int){

    let maxCount = max(oldCount, newCount)
    let minCount = min(oldCount, newCount)

    var changed = [NSIndexPath]()

    for i in minCount..<maxCount {
        let indexPath = NSIndexPath(forRow: i, inSection: section)
        changed.append(indexPath)
    }

    var reload = [NSIndexPath]()
    for i in 0..<minCount{
        let indexPath = NSIndexPath(forRow: i, inSection: section)
        reload.append(indexPath)
    }

    beginUpdates()
    if(newCount > oldCount){
        insertRowsAtIndexPaths(changed, withRowAnimation: .Fade)
    }else if(oldCount > newCount){
        deleteRowsAtIndexPaths(changed, withRowAnimation: .Fade)
    }
    if(newCount > oldCount || newCount == oldCount){
        reloadRowsAtIndexPaths(reload, withRowAnimation: .None)
    }
    endUpdates()

}
user160917
  • 9,211
  • 4
  • 53
  • 63
3

Try this:

BOOL needsReloadHeader = YES;
UIView *oldHeaderView = nil;

-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
    UIView *headerToReturn = nil;
    if(needsReloadHeader == YES) {
        headerToReturn = [[UIView alloc] init];
        // ...
        // custom your header view in this block
        // and save
        // ...
        oldHeaderView = headerToReturn;
    } else {
        headerToReturn = oldHeaderView;
    }
    return headerToReturn;
}

Your just need to change 'needsReloadHeader' to 'NO' in other places.

ziggear
  • 886
  • 7
  • 22
  • I am doing more or less the same thing. The problem is that the header is still being animated when calling `tableView:reloadSections:withRowAnimation:`. With `UITableViewRowAnimationLeft` for example, the header slides out to the left and then slides in from the right again. –  Dec 27 '13 at 17:01
  • Sorry but I have no idea for this :) ... maybe your can try implement your custom 'reload' methods. – ziggear Dec 27 '13 at 17:10
1

An objective-c version of Intentss extension

@interface UITableView (Extensions)

- (void)reloadRowsInSection:(NSUInteger)sectionIndex withRowAnimation:(UITableViewRowAnimation)rowAnimation oldCount:(NSUInteger)oldCount newCount:(NSUInteger)newCount;

@end


@implementation UITableView (Extensions)

- (void)reloadRowsInSection:(NSUInteger)sectionIndex withRowAnimation:(UITableViewRowAnimation)rowAnimation oldCount:(NSUInteger)oldCount newCount:(NSUInteger)newCount {

    NSUInteger minCount = MIN(oldCount, newCount);

    NSMutableArray *insert = [NSMutableArray array];
    NSMutableArray *delete = [NSMutableArray array];
    NSMutableArray *reload = [NSMutableArray array];

    for (NSUInteger row = oldCount; row < newCount; row++) {
        [insert addObject:[NSIndexPath indexPathForRow:row inSection:sectionIndex]];
    }

    for (NSUInteger row = newCount; row < oldCount; row++) {
        [delete addObject:[NSIndexPath indexPathForRow:row inSection:sectionIndex]];
    }

    for (NSUInteger row = 0; row < minCount; row++) {
        [reload addObject:[NSIndexPath indexPathForRow:row inSection:sectionIndex]];
    }

    [self beginUpdates];

    [self insertRowsAtIndexPaths:insert withRowAnimation:rowAnimation];
    [self deleteRowsAtIndexPaths:delete withRowAnimation:rowAnimation];
    [self reloadRowsAtIndexPaths:reload withRowAnimation:rowAnimation];

    [self endUpdates];
}

@end
Jochen
  • 606
  • 6
  • 23
0

You're reloading the section, so clearly everything in the section will be reloaded (including the header).

Why not instead place the UISegmentedControl inside UITableView's tableHeaderView? This would allow for exactly the behavior you're after.

mattyohe
  • 1,814
  • 12
  • 15
  • This would probably work perfectly. However, I wish a behaviour like in the App Store app, where, as the user scrolls, the title on the top scrolls away, but the `segmentedControl` sticks right under the `navigationBar`. Therefore, I have separated the title and the segmentedControl into two different headers. So I guess this wouldn't work no longer with your method... Thanks anyway :) –  Dec 27 '13 at 15:45
  • Then you might as well create a custom layout with UICollectionView. You can pull off sticky headers pretty easily with custom UICollectionViewLayouts. You should also mention in your question your additional requirements. – mattyohe Dec 27 '13 at 16:13
0

The simple answer is just don't reload the sections animated, just use UITableViewRowAnimationNone.

Right now you're using UITableViewRowAnimationLeft and UITableViewRowAnimationRight, which slides your section in and out as well.

However, even with UITableViewRowAnimationNone, rows will still be animated if the number of cells before the update differ from the ones after the update.

Also, a nice read on this topic, here.

Cheers.

Alin
  • 9
  • 2
0

Here's another way which you could use and still use animations.

Let's say you have a dynamic DataSource, which changes when you select something, and you want to update just the rows of that section, while leaving the section header on top, untouched.

/** I get the desired handler from the handler collection. This handler is just a
 simple NSObject subclass subscribed to UITableViewDelegate and UITableViewDataSource
 protocols. **/
id handler = [self.tableViewHandlers objectForKey:[NSNumber numberWithInteger:index]];

/**  Get the rows which will be deleted  */
NSInteger numberOfRows = [self.tableView numberOfRowsInSection:sectionIndex];
NSMutableArray* indexPathArray = [NSMutableArray array];

for (int rowIndex = 0; rowIndex < numberOfRows; rowIndex++){
    [indexPathArray addObject:[NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex]];
}

/**  Update the handler  */
[self.tableView setDataSource:handler];
[self.tableView setDelegate:handler];

/**  Get the rows which will be added  */
NSInteger newNumberOfRows = [handler tableView:self.tableView numberOfRowsInSection:sectionIndex];
NSMutableArray* newIndexPathArray = [NSMutableArray array];

for (int rowIndex = 0; rowIndex < newNumberOfRows; rowIndex++){
    [newIndexPathArray addObject:[NSIndexPath indexPathForRow:rowIndex inSection:sectionIndex]];
}

/**  Perform updates  */
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:indexPathArray withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:newIndexPathArray withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];

As a note, please stick to the specified order of operations, UITableView demands it. If you have only one handler (datasource and delegate), it's easy to modify the above code to achieve the same results.

Alin
  • 9
  • 2