UITableView - smooth expand and collapse showing a UICollectionView

246 Views Asked by At

I am working on a feature in an app where the user can tap on some attribute about a car (such as its color) and a list of different colors will be shown.

This is just part of the page. The whole page consists of different views and view controllers all wrapped together in a vertical stack view, but I do not think that is what is causing the animation issues here. This is the overall structure of the page:

  • UIScrollView
    • UIStackView
      • UIViewController
      • UIView
      • UITableViewController <-- this is the class being shown in this question
      • ...

But for this question, I am just showing the section of the page where this table is.

So I am having an issue with the animation of the table as can be seen in the gifs later on. The way this section is structured is that it is a UITableViewController subclass that takes in a model (a title and a list of car colors) and it displays a different table section for each model element.

Each section has a section header, which is the part where the user taps. It shows the color and it shows a preview image. When that is tapped, a table row is added to the table for that section and that change is animated. The table view row that is added to the table contains a UICollectionView that lays out content horizontally. This idea came from the WWDC 2010 - Mastering Table Views video.

It is basically something like this. Each section header is the interactive part where you can tap on it. Then the row that is shown in the section has a collection view.

* SectionHeader
* SectionHeader
  * Table row containing a UICollectionView
* SectionHeader

The problem I am having is that the animation is behaving in a strange manner, as can be seen in the following images.

The first image shows what happens if a row is expanded that has other rows below it. In this case, it appears as if the other two rows in the table are essentially floating above the content of the new row that is animating in. Additionally, at the bottom of the section, those two rows come in from the bottom as the previous two rows sort of get vertically squished until they are gone.

Top row animation

The second animation here shows the bottom row expanding. This one is closer to what I want, except that the row content still briefly shows at the top of the row above the divider (which is the section footer of the previous section).

Bottom row animation

Here is my code for the table view controller class.

MyAttributeTableViewController.h

#import <UIKit/UIKit.h>
#import "MyAttribute.h"
#import "MyAttributeTableViewCell.h"

NS_ASSUME_NONNULL_BEGIN

@interface MyAttributeTableViewController : UITableViewController<MyAttributeDelegate>

//this represents each section of the table. It contains a title (color, for example), a selected value (red),
//and a list of possible values that will be used to display the collection view when the section is expanded.
@property (strong, nonatomic) NSArray<MyAttribute *> *modelAttributes;

@end

NS_ASSUME_NONNULL_END

MyAttributeTableViewController.m

#import "MyAttributeTableViewController.h"
#import "MyAttributeTableHeaderView.h"
#import "MyAttributeTableViewCell.h"

@interface MyAttributeTableViewController ()

//this keeps track of which sections are expanded. Initially, all sections start out
//not expanded. Then, upon tap, a section's expanded status is toggled here. This is
//used by the table view's data source to know when to display a row in a section.
@property (strong, nonatomic) NSMutableArray<NSNumber *> *itemsExpanded;

@end

@implementation MyAttributeTableViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.tableView.rowHeight = UITableViewAutomaticDimension;
    self.tableView.estimatedRowHeight = UITableViewAutomaticDimension;
    self.tableView.sectionHeaderHeight = UITableViewAutomaticDimension;
    self.tableView.estimatedSectionHeaderHeight = UITableViewAutomaticDimension;
    
    self.tableView.estimatedSectionFooterHeight = CGFLOAT_MIN;
    
    //this xib file only contains the UICollectionView that is shown upon expanding the section.
    //There is nothing that interesting in this file, but I have some screenshots of the
    //attribute inspector for this xib file after this code.
    [self.tableView registerNib:[UINib nibWithNibName:@"MyAttributeTableViewCell" bundle:nil] forCellReuseIdentifier:@"MyAttributeTableViewCell"];
    [self.tableView registerClass:[MyAttributeTableHeaderView class] forHeaderFooterViewReuseIdentifier:@"AttributeHeaderView"];
    [self.tableView invalidateIntrinsicContentSize];
}

- (void)setAttributeOptions:(NSArray<MyAttribute *> *)modelAttributes {
    self->_modelAttributes = modelAttributes;
    
    self->_itemsExpanded = [NSMutableArray arrayWithCapacity:[self->_modelAttributes count]];
    for (NSUInteger x=0; x<[self->_modelAttributes count]; x++) {
        [self->_itemsExpanded addObject:@NO];
    }
    
    [self.tableView reloadData];
}


//TODO: is this necessary?
- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    self.preferredContentSize = self.tableView.contentSize;
}

#pragma mark - MyAttributeDelegate

- (void)twister:(MyAttributeTableViewCell *)cell didSelectItemAtIndex:(NSInteger)index {
    UITableViewHeaderFooterView *headerView = [self.tableView headerViewForSection:cell.sectionIndex];
    if ([headerView isKindOfClass:[MyAttributeTableHeaderView class]]) {
        MyAttributeTableHeaderView *twisterHeaderView = (MyAttributeTableHeaderView *)headerView;
        twisterHeaderView.twister.selectedSwatchIndex = [NSNumber numberWithInteger:index];
        [twisterHeaderView updateAttributeIfNeeded];
    }
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return self.modelAttributes.count;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.itemsExpanded[section].boolValue ? 1 : 0;
}

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    MyAttributeTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyAttributeTableViewCell" forIndexPath:indexPath];
    MyAttribute *twister = self.modelAttributes[indexPath.section];
    cell.swatches = twister.swatches;
    NSNumber *swatchCellHeight = cell.swatchCellHeight;
    if (swatchCellHeight) {
        return swatchCellHeight.floatValue + 20.0f; //TODO: need to get this from the collection view
    }
    return 352.0f;
}

- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
    if (section == self.modelAttributes.count) {
        return CGFLOAT_MIN;
    }
    return 1.0f;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    MyAttributeTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyAttributeTableViewCell" forIndexPath:indexPath];
    MyAttribute *twister = self.modelAttributes[indexPath.section];
    cell.swatches = twister.swatches;
    cell.sectionIndex = indexPath.section;
    cell.delegate = self;
    
    if ([twister isSwatchSelected]) {
        NSInteger selectedIndex = twister.selectedSwatchIndex.integerValue;
        [cell.collectionView selectItemAtIndexPath:[NSIndexPath indexPathForItem:selectedIndex inSection:0] animated:YES scrollPosition:UICollectionViewScrollPositionCenteredHorizontally];
    }
    return cell;
}

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
    MyAttributeTableHeaderView *cell = (MyAttributeTableHeaderView *)[tableView dequeueReusableHeaderFooterViewWithIdentifier:@"AttributeHeaderView"];
    
    if (cell.gestureRecognizers.count == 0) {
        UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(toggleSection:)];
        [cell addGestureRecognizer:tapRecognizer];
    }
    cell.sectionIndex = section;
    cell.expanded = self.itemsExpanded[section].boolValue;
    cell.twister = self.modelAttributes[section];
    return cell;
}

- (void)toggleSection:(UITapGestureRecognizer *)gesture {
    MyAttributeTableHeaderView *destinationView = ((MyAttributeTableHeaderView *)gesture.view);
    BOOL expanded = [destinationView toggleExpanded];
    self.itemsExpanded[destinationView.sectionIndex] = [NSNumber numberWithBool:expanded];

    
    UIView *viewToLayout = self.tableView;
    while ([viewToLayout superview]) {
        viewToLayout = viewToLayout.superview;
    }
    
    if (expanded) {
        [UIView beginAnimations:@"expandTableAnimationId" context:nil];
        [UIView setAnimationDuration:1.0f];
        [CATransaction begin];
        

        [self.tableView beginUpdates];
        [self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:0 inSection:destinationView.sectionIndex]] withRowAnimation:UITableViewRowAnimationNone];
        [self.tableView endUpdates];

        [viewToLayout layoutIfNeeded];
        
        [CATransaction commit];
        [UIView commitAnimations];
    } else {
        [UIView beginAnimations:@"collapseTableAnimationId" context:nil];
        [UIView setAnimationDuration:1.0f];
        [CATransaction begin];
        
        
        [self.tableView beginUpdates];
        [self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:0 inSection:destinationView.sectionIndex]] withRowAnimation:UITableViewRowAnimationNone];
        [self.tableView endUpdates];

        [viewToLayout layoutIfNeeded];

        [CATransaction commit];
        [UIView commitAnimations];
    }
}

@end

MyAttributeTableViewCell

MyAttributeTableViewCell attribute inspector

Does anyone know what I am doing wrong or how to get these animations to look correct (no doubling of rows and no collection view showing up above the row when it is animating in)? Or if you know a better way to handle this that might be less complicated, I am open to that as well. I am just trying to have a list of expandable sections, where each section has a collection view of selectable items.

0

There are 0 best solutions below