【问题标题】:UITableView in UIStackView - smooth expand and collapse animationUIStackView 中的 UITableView - 平滑展开和折叠动画
【发布时间】:2024-01-11 07:38:01
【问题描述】:

我正在尝试实现一个表格视图,允许用户“显示更多”行来切换完整的行数或较少的行数。加载视图时我已经预先准备好所有数据,因此我不需要获取任何额外的数据。

我可以让它工作,但我遇到的问题是我的表格视图在展开或折叠时动画不流畅。发生的情况是,如果触发“显示更多”操作,表格的大小会立即更新到包含所有数据的表格的完整高度。然后行将动画。

同样在隐藏行时,表格高度将一次全部缩小到结束高度,然后行将动画。

这是正在发生的事情的图片。它只是“跳”到全高,然后为行设置动画。但我希望它能够平稳地扩展表的高度,从而在向下平稳地扩展时显示数据。我想要相反的,当按下“少显示”时它会平滑地向上滑动。

我的应用布局如下:

  • UIScrollView
    • UIStackView
      • UIViewController
      • UIViewController
      • UITableView
      • UIView
      • ...

基本上,我有一个不同数据部分的可滚动列表。有些部分是表格,有些只是使用 AutoLayout 排列的临时视图,其他部分具有集合视图或页面视图控制器或其他类型的视图。

但是这部分是由上一个列表中的 UITableView 来表示的。

#import "MyTableView.h"
#import "MyAutosizingTableView.h"

@interface MyTableView ()

@property (strong, nonatomic) UILabel *titleLabel;
@property (strong, nonatomic) MyAutosizingTableView *tableView;

@end

@implementation MyTableView

- (instancetype)init {
    return [self initWithFrame:CGRectZero];
}

- (instancetype)initWithCoder:(NSCoder *)coder {
    if (self = [super initWithCoder:coder]) {
        [self initialize];
    }
    return self;
}

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self initialize];
    }
    return self;
}

- (void)initialize {
    [self.titleLabel setText:@"Details"];
    self.numberOfRows = 3;
    [self addSubview:self.titleLabel];
    [self addSubview:self.tableView];
    
    [NSLayoutConstraint activateConstraints:@[
        [self.titleLabel.leadingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.leadingAnchor],
        [self.titleLabel.topAnchor constraintEqualToAnchor:self.layoutMarginsGuide.topAnchor],
        [self.titleLabel.trailingAnchor constraintEqualToAnchor:self.layoutMarginsGuide.trailingAnchor],
        
        [self.tableView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
        [self.tableView.topAnchor constraintEqualToSystemSpacingBelowAnchor:self.titleLabel.bottomAnchor multiplier:1.0f],
        [self.tableView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
        [self.tableView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
        [self.tableView.widthAnchor constraintEqualToAnchor:self.widthAnchor]
    ]];
}

- (UITableView *)tableView {
    if (!self->_tableView) {
        self->_tableView = [[MyAutosizingTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
        self->_tableView.translatesAutoresizingMaskIntoConstraints = NO;
        self->_tableView.rowHeight = UITableViewAutomaticDimension;
        self->_tableView.estimatedRowHeight = UITableViewAutomaticDimension;
        self->_tableView.allowsSelection = NO;
        self->_tableView.scrollEnabled = NO;
        self->_tableView.delegate = self;
        self->_tableView.dataSource = self;
        [self->_tableView registerClass:[MyTableViewCell class] forCellReuseIdentifier:@"dataCell"];
    }
    return self->_tableView;
}

- (UILabel *)titleLabel {
    if (!self->_titleLabel) {
        self->_titleLabel = [[UILabel alloc] init];
        self->_titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
        self->_titleLabel.numberOfLines = 0;
        self->_titleLabel.userInteractionEnabled = YES;
        self->_titleLabel.textAlignment = NSTextAlignmentNatural;
        self->_titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
        self->_titleLabel.baselineAdjustment = UIBaselineAdjustmentAlignBaselines;
        self->_titleLabel.adjustsFontSizeToFitWidth = NO;
        
        UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(showMore)];
        [self->_titleLabel addGestureRecognizer:tapGesture];
    }
    return self->_titleLabel;
}

- (void)showMore {
    if (self.numberOfRows == 0) {
        self.numberOfRows = 3;
    } else {
        self.numberOfRows = 0;
    }
    
    NSIndexSet *indexes = [NSIndexSet indexSetWithIndex:0];
    [self.tableView reloadSections:indexes withRowAnimation:UITableViewRowAnimationFade];
}

- (void)setData:(NSArray<NSString *> *)data {
    self->_data = data;
    [self.tableView reloadData];
}


- (void)didMoveToSuperview {
    //this has no effect unless the view is already in the view hierarchy
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
}

#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    if (self.numberOfRows <= 0) {
        return self.data.count;
    }
    return MIN(self.data.count, self.numberOfRows);
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *data = self.data[indexPath.row];
    MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"dataCell" forIndexPath:indexPath];
    cell.data = data;
    return cell;
}

@end

我有一个 UITableView 子类,我在之前的代码中使用过。我有表格视图子类的原因是让表格在内容更改时调整大小。如果有人知道没有子类的更好方法,我也会非常有兴趣学习。

#import "MyAutosizingTableView.h"

@implementation MyAutosizingTableView

- (CGSize)intrinsicContentSize {
    return self.contentSize;
}

- (void)setContentSize:(CGSize)contentSize {
    [super setContentSize:contentSize];
    [self invalidateIntrinsicContentSize];
}

@end

有谁知道我可以做些什么来获得平滑的展开动画?我真的希望表格能够顺利展开并显示新行中的数据。


更新 - 我正在尝试做的高级概述

在这里,我将尝试描述我正在尝试完成的工作以及我将如何进行,因此,如果有人有任何替代方法或改进来解决这个问题,我将非常愿意接受建议。提前感谢您阅读本文。

所以我正在开发一个显示产品数据的视图控制器,例如汽车。视图控制器有不同的部分负责显示不同的数据。例如,第一部分显示商品的图片,对于这一部分,我使用的是 UIPageView,它基本上显示产品图像的轮播。

另一个部分显示有关当前产品的选项。例如,有人可能会为汽车或不同的车轮选择不同的颜色。对于本节,我使用表格视图来显示可用属性的列表,其中每个属性位于表格的不同部分。按下节标题后,表格视图会添加一行,显示该属性的可用选项。例如,假设汽车有不同的可用颜色(红色、绿色、蓝色和黄色)。按下节标题后,表格视图会添加一行并对其进行动画处理(使用“iOS 12 编程”第 8 章中讨论的技术)。然后显示的行包含一个水平滚动的 UICollectionView,允许用户在颜色(或滚轮选项或正在更改的任何属性)之间进行选择。

另一个部分显示其他客户对该产品的评价。例如,如果有人在汽车上留下了一篇文章,那么这将放在本节中。本部分有一个表格,显示产品在不同标准下的票价。仍然以汽车为例,它可能有一排是为了舒适,另一排是为了油耗,另一排是为了性能等等。然后还有一个水平的 UICollectionView 显示关于产品的文章。

还有一个部分是产品的属性列表。例如,对于汽车,它可能会在左侧显示发动机尺寸,在右侧显示 V12。

还有其他部分,但是为了将它们结合在一起,我有一个滚动视图,里面有一个垂直的 UIStackView。这就是我真正好奇的地方。显示或布局所有这些部分的最佳方式是什么?堆栈视图是要走的路,还是我应该使用表格视图,还是应该只使用滚动视图与这些视图挂钩,直接使用 AutoLayout,还是有比所有这些更好的方法?

非常重要的一点是,每个部分在产品之间可以有不同的尺寸。例如,某些部分可能是一种产品的一个高度,但对于其他产品可能是完全不同的高度。所以我需要它能够动态调整每个部分的大小,并且我还需要能够为一些大小更改设置动画(例如,它向表中添加一行然后删除它的部分,它需要动画并制作整个部分以动画方式放大)。

另外,我希望每个部分都是模块化的,并且最终不会有一个巨大的视图控制器来管理所有内容。我希望每个部分基本上都是独立的。我只是将数据传递给该部分,然后它处理与布局和交互相关的所有内容。但同样,真正的问题是让这些视图能够调整大小并在父视图(当前为堆栈视图)中进行更新。

但我对 iOS 开发真的很陌生,虽然我一直在努力尽可能快地学习,但我仍然不确定我目前这样做的方式(其中包含堆栈视图的滚动视图) 是最好的方法,或者如果有更好的方法。我最初在一个带有静态单元格的表格视图中尝试了这个,其中每一行都是不同的部分。但是随后让子视图控制器调整父表视图中的行大小并不能很好地工作。从那时起,我也从让每个部分成为单独的视图控制器,而只是让每个部分成为一个视图(而不是视图控制器),以使调整大小更容易,但我仍然遇到问题。我也考虑过集合视图,但我还需要能够支持 iOS 12(或者如果我真的需要并且只支持 iOS 13+,我可以放弃对 iOS 12 的支持)。

但重申一下,我的总体目标是有一些方法来布局这个界面,该界面由不同的视图组成,每个视图处理不同的数据。我希望每个视图都能够调整大小并且能够平滑。而且我希望界面的每个部分都是模块化的,以避免使用一个大视图控制器。

更新 2

按照建议将表格视图包装在 UIView 中,然后设置具有不同优先级的两个底部约束,这就是我得到的。每当我尝试为视图的高度约束等设置动画时,我也会得到这种效果。我认为这是因为这里的整体视图包含在堆栈视图中,所以我认为堆栈视图在其排列的子视图更改大小时正在做一些事情。

这里的图片显示了展开和折叠表格视图。折叠时可见三行,展开时可见五行。

黄色和蓝色视图代表页面上的其他视图。它们不是该视图的一部分,它们所包含的堆栈视图也不是。它们只是此处包含的页面的其他部分,用于显示此问题。这里的所有视图都包含在堆栈视图中,我认为这是导致动画问题的原因,但我不确定如何解决。

【问题讨论】:

  • 一个更好的解决方法是使用 UICollectionView 自定义您的可扩展表格,同时扩展只需调用 reloadSections(_:with:) 重新加载特定部分。
  • 从您的问题中不清楚您要“展开/折叠”什么。您的动画图像似乎显示表格行 moving ...您只是想“显示”已经填充的表格视图?
  • @DonMag 对 gif 的质量感到抱歉,它太快了。但是发生的事情是我们从 3 行开始,然后当我展开表格时,表格似乎在添加任何行之前设置了它的 frame.height,然后它添加了行。但这会导致该表部分下方的其余部分“跳”下来而不是平滑地动画。我还尝试让表格完全填充,然后在表格上添加高度约束,但这也导致了跳跃,因为它包含在堆栈视图中。有没有比使用堆栈视图更好的布局接口的方法?跨度>
  • @pqteru 你的意思是使用 UICollectionView 来处理应用程序的布局吗?我尝试过使用 UITableView 和静态单元格,到目前为止我尝试过使用堆栈视图。当我需要调整其中一个部分的大小时(它可能是 UIViewController 子类或 UIView 子类),它们都会给我带来问题。但我需要能够支持在这些部分中动态扩展和折叠项目,因此这些部分需要能够增加和缩小高度。我也在尝试支持 iOS 12+,但如有必要,我可以放弃它并支持 iOS 13+。你推荐什么样的布局?
  • @DonMag 或者你发现我的代码有什么问题吗?当我将所有行添加到表视图中,然后通过约束设置表的高度然后为其设置动画时,如果我将包含视图设置为非常大的高度(对于本节,我有一个 UIView 和在其中我有一个标签和一个表格,如代码所示),因此它不必扩展。但是如果表格视图必须拉伸包含视图,那就是它停止正常工作的时候。我只是想让该部分自动调整大小,并使其能够根据需要顺利展开和折叠表格。

标签: ios uitableview uikit uistackview uianimation


【解决方案1】:

您在这里确实有很多问题 - 但要专门解决尝试展开/折叠表格视图的问题...

我认为你会用这种方法打一场失败的战斗。通过尝试使用“自动调整大小”的表格视图,您可以抵消大部分表格视图的行为/而且,由于您没有从可重用单元格的内存管理中受益,您可能最好格式化您的带有垂直堆栈视图的“规范列表”。

在任何一种情况下,要创建具有“显示/隐藏”效果的折叠/展开动画,您可能希望将 tableView 或 stackView 嵌入“容器”视图中,然后切换高度的约束优先级(底部) 的容器。

这是一个简单的例子......

  • 创建一个“容器”UIView
  • 向容器添加垂直堆栈视图
  • 向堆栈视图添加标签
  • 在容器上方添加一个红色的UIView
  • 在容器下方添加蓝色UIView
  • 定义容器高度的约束

每次点击时,容器都会展开/折叠以显示/隐藏标签。

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController
@end

@interface ViewController ()

@property (strong, nonatomic) NSLayoutConstraint *collapsedConstraint;
@property (strong, nonatomic) NSLayoutConstraint *expandedConstraint;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIStackView *labelsStackView = [UIStackView new];
    labelsStackView.axis = UILayoutConstraintAxisVertical;
    labelsStackView.spacing = 4;
    
    // let's add 12 labels to the stack view (12 "rows")
    for (int i = 1; i < 12; i++) {
        UILabel *v = [UILabel new];
        v.text = [NSString stringWithFormat:@"Label %d", i];
        [labelsStackView addArrangedSubview:v];
    }
    
    // add a view to show above the stack view
    UIView *redView = [UIView new];
    redView.backgroundColor = [UIColor redColor];
    
    // add a view to show below the stack view
    UIView *blueView = [UIView new];
    blueView.backgroundColor = [UIColor blueColor];

    // add a "container" view for the stack view
    UIView *cView = [UIView new];
    
    // clip the container's subviews
    cView.clipsToBounds = YES;
    
    for (UIView *v in @[redView, cView, blueView]) {
        v.translatesAutoresizingMaskIntoConstraints = NO;
        [self.view addSubview:v];
    }
    
    // add the stackView to the container
    labelsStackView.translatesAutoresizingMaskIntoConstraints = NO;
    [cView addSubview:labelsStackView];
    
    // constraints
    UILayoutGuide *g = [self.view safeAreaLayoutGuide];
    
    // when expanded, we'll use the full height of the stack view
    _expandedConstraint = [cView.bottomAnchor constraintEqualToAnchor:labelsStackView.bottomAnchor];
    
    // when collapsed, we'll use the bottom of the 3rd label in the stack view
    UILabel *v = labelsStackView.arrangedSubviews[2];
    _collapsedConstraint = [cView.bottomAnchor constraintEqualToAnchor:v.bottomAnchor];
    
    // start collapsed
    _expandedConstraint.priority = UILayoutPriorityDefaultLow;
    _collapsedConstraint.priority = UILayoutPriorityDefaultHigh;
    
    [NSLayoutConstraint activateConstraints:@[
        
        // redView Top / Leading / Trailing / Height=120
        [redView.topAnchor constraintEqualToAnchor:g.topAnchor constant:20.0],
        [redView.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:20.0],
        [redView.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-20.0],
        [redView.heightAnchor constraintEqualToConstant:120.0],
        
        // container Top==redView.bottom / Leading / Trailing / no height
        [cView.topAnchor constraintEqualToAnchor:redView.bottomAnchor constant:0.0],
        [cView.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:20.0],
        [cView.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-20.0],
        
        // blueView Top==stackView.bottom / Leading / Trailing / Height=160
        [blueView.topAnchor constraintEqualToAnchor:cView.bottomAnchor constant:0.0],
        [blueView.leadingAnchor constraintEqualToAnchor:g.leadingAnchor constant:20.0],
        [blueView.trailingAnchor constraintEqualToAnchor:g.trailingAnchor constant:-20.0],
        [blueView.heightAnchor constraintEqualToConstant:160.0],
        
        // stackView Top / Leading / Trailing
        [labelsStackView.topAnchor constraintEqualToAnchor:cView.topAnchor],
        [labelsStackView.leadingAnchor constraintEqualToAnchor:cView.leadingAnchor],
        [labelsStackView.trailingAnchor constraintEqualToAnchor:cView.trailingAnchor],

        _expandedConstraint,
        _collapsedConstraint,
        
    ]];

}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    // toggle priority between expanded / collapsed constraints
    if (_expandedConstraint.priority == UILayoutPriorityDefaultHigh) {
        _expandedConstraint.priority = UILayoutPriorityDefaultLow;
        _collapsedConstraint.priority = UILayoutPriorityDefaultHigh;
    } else {
        _collapsedConstraint.priority = UILayoutPriorityDefaultLow;
        _expandedConstraint.priority = UILayoutPriorityDefaultHigh;
    }
    // animate the change
    [UIView animateWithDuration:0.5 animations:^{
        [self.view layoutIfNeeded];
    }];
    
}

@end

也许可以使用“自动调整大小”表格视图来实现这一点,但同样,因为您没有使用“正常”表格视图行为(滚动、可重复使用的单元格等) ),这可能是更好的方法。

希望它还可以让您深入了解布局中其他视图的大小 - 特别是如果您想在视图中或视图之外对其进行动画处理。

编辑 - 在此处添加了一个更复杂的示例:https://github.com/DonMag/Scratch2021

它使用垂直滚动视图中的垂直堆栈视图作为“主视图”UI,在堆栈视图中添加子视图控制器作为“组件”。

【讨论】:

  • 非常感谢您在这里的帮助。我会试试这个。但是,您对我总体布局所有这些的方式(使用堆栈视图)是否正确或是否有更好的方法有任何建议?您提到了由于没有以正确的方式使用表格而失去的记忆优势。如果您必须布置这样的页面(将其想象成零售网站上的产品页面,其中有图片、描述、评论……),您会怎么做?我希望它由较小的视图组成,就像这样,这样我就可以让每个视图保持单一的焦点和可管理性。谢谢。
  • 我刚刚尝试了您的建议,将 tableview 包装在 UIView 中,然后在其上添加折叠和展开的底部约束,它可以工作,但是在制作动画时,父堆栈视图的动画效果不正确。我以前遇到过这个问题,无法解决。此类定义的整体视图被添加为父堆栈视图的排列子视图,用于布局页面的所有部分。我为题为 Update 2 的问题添加了一个更新,其中包含一个显示动画的 gif。你知道堆栈视图中是否有一些设置可能导致这种情况吗?谢谢。
  • @ashipma - 没有看到你的全部尝试,很难说。我提出了一个更复杂的示例,更接近您的 UI 和方法:github.com/DonMag/Scratch2021
  • 非常感谢您将所有这些放在一起并花费大量时间和精力来完成此操作。我真的很感谢你的帮助。再一次,你解决了我的问题。我将遍历添加到根视图(或者在这种情况下,我可以在我想的父堆栈视图处停止)并且修复了所有内容。我不完全确定堆栈视图在内部是如何工作的,但我想如果我们为改变排列子视图高度的任何东西设置动画,那么我们需要布局整个堆栈视图。我以前只是布置子视图,但那不起作用。再次感谢您。
  • 嘿@DonMag - 我刚刚发布了一个与此类似的问题(它再次与同一个项目的动画有关)。这次我有一个带有可扩展部分的表格视图的部分。每个部分都包含一行,它是一个集合视图。但是动画有点奇怪。如果您不想,您绝对不需要查看它,但我非常感谢您可能拥有的任何见解或任何有关替代解决方案的建议。太感谢了。 *.com/questions/67541899/…
最近更新 更多