UIStackView 中的 UITableView - 流畅的展开和折叠动画
UITableView in UIStackView - smooth expand and collapse animation
我正在尝试实现一个 table 视图,该视图允许用户“显示更多”行以切换完整行数或较少行数。我在加载视图时预先拥有所有数据,因此我不需要获取任何额外数据。
我能够让它工作,但我遇到的问题是我的 table 视图在展开或折叠时不能流畅地动画。发生的情况是,如果触发“显示更多”操作,table 的大小会立即更新为包含所有数据的 table 的完整高度。然后行将动画。
同样,当隐藏行时,table 高度会立即收缩到结束高度,然后行会动画。
这是正在发生的事情的图片。它只是“跳”到全高,然后为行设置动画。但我希望发生的事情是它平滑地扩展 table 的高度,在它平滑地向下扩展时显示数据。我想要相反的,当按下“show less”时它平滑地向上滑动。
我的应用布局方式如下:
UIScrollView
UIStackView
UIViewController
UIViewController
UITableView
<-- 我正在处理的部分
UIView
- ...
基本上,我有一个包含不同数据部分的可滚动列表。有些部分是 table,有些只是 ad-hoc 使用 AutoLayout 排列的视图,其他部分有 collection 视图或页面视图控制器或其他类型的视图。
但是这部分是由前面列表中的 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 子类。我拥有 table 视图子类的原因是让 table 在内容更改时调整大小。如果有人知道在没有子类的情况下执行此操作的更好方法,我也会非常有兴趣学习它。
#import "MyAutosizingTableView.h"
@implementation MyAutosizingTableView
- (CGSize)intrinsicContentSize {
return self.contentSize;
}
- (void)setContentSize:(CGSize)contentSize {
[super setContentSize:contentSize];
[self invalidateIntrinsicContentSize];
}
@end
有谁知道我可以做些什么来获得流畅的展开动画?我真的希望 table 能够顺利扩展并显示新行中的数据。
更新 - 对我正在尝试做的事情的高级概述
在这里,我将尝试描述我要完成的工作以及我将如何进行,所以如果有人有任何替代方法或改进来解决这个问题,我会非常乐意接受建议.预先感谢您阅读本文。
所以我正在研究一个显示产品数据的视图控制器,例如想象一辆汽车。视图控制器有不同的部分负责显示不同的数据。例如,第一部分显示商品的图片,而对于这一部分,我使用的是 UIPageView,它基本上显示产品图片的轮播。
另一部分显示有关当前产品的选项。例如,有人可能会为汽车或不同的车轮选择不同的颜色。对于此部分,我使用 table 视图来显示可用属性列表,其中每个属性位于 table 的不同部分。按 header 部分后,table 视图会添加一行,显示该属性的可用选项。例如,假设汽车有不同的颜色(红色、绿色、蓝色和黄色)。按下 header 部分后,table 视图会添加一行并为其添加动画(使用“编程 iOS 12”第 8 章中讨论的技术)。然后显示的行包含一个水平滚动的 UICollectionView,允许用户在颜色(或滚轮选项或正在更改的任何属性)之间进行选择。
另一部分显示其他客户对此产品的评价。例如,如果有人在汽车上留下了评论,那么它就会出现在本节中。此部分有一个 table 显示产品在不同标准下的表现。仍然以汽车为例,它可能有一行用于舒适度,另一行用于油耗,另一行用于性能等等。然后还有一个水平的 UICollectionView 显示关于产品的评论。
还有一个部分是产品的属性列表。例如,对于汽车,它可能会在左侧显示发动机尺寸,在右侧显示 V12。
还有其他部分,但为了将它们联系在一起,我有一个滚动视图,其中有一个垂直的 UIStackView。这就是我真正好奇的地方。显示或布局所有这些部分的最佳方式是什么?堆栈视图是要走的路还是应该使用 table 视图,或者我应该直接使用 AutoLayout 将这些视图连接在一起的滚动视图,还是有比所有这些更好的方法来做到这一点?
一件非常重要的事情是,每个部分在产品之间可以有不同的尺寸。前面例如,某些部分对于一种产品可能是一个高度,但对于其他产品则可能是完全不同的高度。所以我需要它能够动态调整每个部分的大小,我还需要能够为一些大小变化设置动画(例如,它向 table 添加一行然后删除它的部分,这需要动画并以动画方式使整个部分变大)。
此外,我希望每个部分都是模块化的,而不是最终拥有一个管理所有内容的巨大视图控制器。我希望每个部分基本上都是独立的。我只是将数据传递给那个部分,然后它处理与布局和交互相关的所有事情。但同样,真正的问题是让这些视图能够调整大小并在 parent 视图(当前堆栈视图)中进行更新。
但我真的是 iOS 开发的新手,虽然我一直在努力尽可能快地学习,但我仍然不确定我目前这样做的方式(滚动视图里面有堆栈视图)是最好的方法,或者如果有更好的方法。我最初在 table 视图中尝试了此操作,其中每行都是不同的部分的静态单元格。但是让 child 视图控制器调整 parent table 视图中的行的大小并没有那么好用。从那时起,我也改变了,不再让每个部分成为一个单独的视图控制器,而是让每个部分成为一个视图(而不是视图控制器),希望能够更轻松地调整大小,但我仍然 运行 遇到问题。我也考虑过 collection 视图,但我还需要能够支持 iOS 12(或者我可以放弃 iOS 12 支持,如果我真的需要并且只支持 iOS 13+).
但重申一下,我的总体目标是通过某种方式来布局此界面,该界面由处理不同数据的不同视图组成。我希望每个视图都能够调整大小并且能够平滑。而且我希望界面的每个部分都是模块化的,以避免一个大视图控制器。
更新 2
按照建议将 table 视图包装在 UIView 中,然后设置两个具有不同优先级的底部约束后,这就是我得到的结果。每当我尝试为诸如视图的高度限制之类的东西设置动画时,我也会得到这种效果。我认为这是因为这里的整体视图包含在堆栈视图中,所以我认为堆栈视图在其排列的子视图更改大小时正在做一些事情。
此处的图片同时显示了 table 视图的展开和折叠。折叠时显示三行,展开时显示五行。
黄色和蓝色视图代表页面上的其他视图。它们不是该视图的一部分,它们所在的堆栈视图也不是。它们只是此处包含的页面的其他部分以显示此问题。这里的所有视图都包含在堆栈视图中,我认为这是导致动画问题的原因,但我不确定如何解决它。
您在这里确实有很多问题 - 但要具体解决尝试 expand/collapse 您的 table 视图的问题...
我认为您将以这种方法打一场必败仗。通过尝试使用“自动调整大小”table 视图,您正在抵消 table 视图的大部分行为/而且,由于您无法通过可重用单元获得内存管理的好处,您最好使用垂直堆栈视图来格式化您的“规格列表”。
在任何一种情况下,要创建具有“显示/隐藏”效果的折叠/展开动画,您可能需要将 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
您可能能够通过您的“自动调整大小”table视图来实现这一点,但是同样,因为您没有使用“正常”table 查看行为(滚动、可重复使用的单元格等),这可能是更好的方法。
希望它还可以让您了解布局中其他视图的大小 - 特别是如果您想要在视图内或视图外设置动画。
编辑 - 在此处添加了一个更复杂的示例:https://github.com/DonMag/Scratch2021
它使用垂直滚动视图中的垂直堆栈视图作为“主视图”UI,在堆栈视图中添加子视图控制器作为“组件”。
我正在尝试实现一个 table 视图,该视图允许用户“显示更多”行以切换完整行数或较少行数。我在加载视图时预先拥有所有数据,因此我不需要获取任何额外数据。
我能够让它工作,但我遇到的问题是我的 table 视图在展开或折叠时不能流畅地动画。发生的情况是,如果触发“显示更多”操作,table 的大小会立即更新为包含所有数据的 table 的完整高度。然后行将动画。
同样,当隐藏行时,table 高度会立即收缩到结束高度,然后行会动画。
这是正在发生的事情的图片。它只是“跳”到全高,然后为行设置动画。但我希望发生的事情是它平滑地扩展 table 的高度,在它平滑地向下扩展时显示数据。我想要相反的,当按下“show less”时它平滑地向上滑动。
我的应用布局方式如下:
UIScrollView
UIStackView
UIViewController
UIViewController
UITableView
<-- 我正在处理的部分UIView
- ...
基本上,我有一个包含不同数据部分的可滚动列表。有些部分是 table,有些只是 ad-hoc 使用 AutoLayout 排列的视图,其他部分有 collection 视图或页面视图控制器或其他类型的视图。
但是这部分是由前面列表中的 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 子类。我拥有 table 视图子类的原因是让 table 在内容更改时调整大小。如果有人知道在没有子类的情况下执行此操作的更好方法,我也会非常有兴趣学习它。
#import "MyAutosizingTableView.h"
@implementation MyAutosizingTableView
- (CGSize)intrinsicContentSize {
return self.contentSize;
}
- (void)setContentSize:(CGSize)contentSize {
[super setContentSize:contentSize];
[self invalidateIntrinsicContentSize];
}
@end
有谁知道我可以做些什么来获得流畅的展开动画?我真的希望 table 能够顺利扩展并显示新行中的数据。
更新 - 对我正在尝试做的事情的高级概述
在这里,我将尝试描述我要完成的工作以及我将如何进行,所以如果有人有任何替代方法或改进来解决这个问题,我会非常乐意接受建议.预先感谢您阅读本文。
所以我正在研究一个显示产品数据的视图控制器,例如想象一辆汽车。视图控制器有不同的部分负责显示不同的数据。例如,第一部分显示商品的图片,而对于这一部分,我使用的是 UIPageView,它基本上显示产品图片的轮播。
另一部分显示有关当前产品的选项。例如,有人可能会为汽车或不同的车轮选择不同的颜色。对于此部分,我使用 table 视图来显示可用属性列表,其中每个属性位于 table 的不同部分。按 header 部分后,table 视图会添加一行,显示该属性的可用选项。例如,假设汽车有不同的颜色(红色、绿色、蓝色和黄色)。按下 header 部分后,table 视图会添加一行并为其添加动画(使用“编程 iOS 12”第 8 章中讨论的技术)。然后显示的行包含一个水平滚动的 UICollectionView,允许用户在颜色(或滚轮选项或正在更改的任何属性)之间进行选择。
另一部分显示其他客户对此产品的评价。例如,如果有人在汽车上留下了评论,那么它就会出现在本节中。此部分有一个 table 显示产品在不同标准下的表现。仍然以汽车为例,它可能有一行用于舒适度,另一行用于油耗,另一行用于性能等等。然后还有一个水平的 UICollectionView 显示关于产品的评论。
还有一个部分是产品的属性列表。例如,对于汽车,它可能会在左侧显示发动机尺寸,在右侧显示 V12。
还有其他部分,但为了将它们联系在一起,我有一个滚动视图,其中有一个垂直的 UIStackView。这就是我真正好奇的地方。显示或布局所有这些部分的最佳方式是什么?堆栈视图是要走的路还是应该使用 table 视图,或者我应该直接使用 AutoLayout 将这些视图连接在一起的滚动视图,还是有比所有这些更好的方法来做到这一点?
一件非常重要的事情是,每个部分在产品之间可以有不同的尺寸。前面例如,某些部分对于一种产品可能是一个高度,但对于其他产品则可能是完全不同的高度。所以我需要它能够动态调整每个部分的大小,我还需要能够为一些大小变化设置动画(例如,它向 table 添加一行然后删除它的部分,这需要动画并以动画方式使整个部分变大)。
此外,我希望每个部分都是模块化的,而不是最终拥有一个管理所有内容的巨大视图控制器。我希望每个部分基本上都是独立的。我只是将数据传递给那个部分,然后它处理与布局和交互相关的所有事情。但同样,真正的问题是让这些视图能够调整大小并在 parent 视图(当前堆栈视图)中进行更新。
但我真的是 iOS 开发的新手,虽然我一直在努力尽可能快地学习,但我仍然不确定我目前这样做的方式(滚动视图里面有堆栈视图)是最好的方法,或者如果有更好的方法。我最初在 table 视图中尝试了此操作,其中每行都是不同的部分的静态单元格。但是让 child 视图控制器调整 parent table 视图中的行的大小并没有那么好用。从那时起,我也改变了,不再让每个部分成为一个单独的视图控制器,而是让每个部分成为一个视图(而不是视图控制器),希望能够更轻松地调整大小,但我仍然 运行 遇到问题。我也考虑过 collection 视图,但我还需要能够支持 iOS 12(或者我可以放弃 iOS 12 支持,如果我真的需要并且只支持 iOS 13+).
但重申一下,我的总体目标是通过某种方式来布局此界面,该界面由处理不同数据的不同视图组成。我希望每个视图都能够调整大小并且能够平滑。而且我希望界面的每个部分都是模块化的,以避免一个大视图控制器。
更新 2
按照建议将 table 视图包装在 UIView 中,然后设置两个具有不同优先级的底部约束后,这就是我得到的结果。每当我尝试为诸如视图的高度限制之类的东西设置动画时,我也会得到这种效果。我认为这是因为这里的整体视图包含在堆栈视图中,所以我认为堆栈视图在其排列的子视图更改大小时正在做一些事情。
此处的图片同时显示了 table 视图的展开和折叠。折叠时显示三行,展开时显示五行。
黄色和蓝色视图代表页面上的其他视图。它们不是该视图的一部分,它们所在的堆栈视图也不是。它们只是此处包含的页面的其他部分以显示此问题。这里的所有视图都包含在堆栈视图中,我认为这是导致动画问题的原因,但我不确定如何解决它。
您在这里确实有很多问题 - 但要具体解决尝试 expand/collapse 您的 table 视图的问题...
我认为您将以这种方法打一场必败仗。通过尝试使用“自动调整大小”table 视图,您正在抵消 table 视图的大部分行为/而且,由于您无法通过可重用单元获得内存管理的好处,您最好使用垂直堆栈视图来格式化您的“规格列表”。
在任何一种情况下,要创建具有“显示/隐藏”效果的折叠/展开动画,您可能需要将 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
您可能能够通过您的“自动调整大小”table视图来实现这一点,但是同样,因为您没有使用“正常”table 查看行为(滚动、可重复使用的单元格等),这可能是更好的方法。
希望它还可以让您了解布局中其他视图的大小 - 特别是如果您想要在视图内或视图外设置动画。
编辑 - 在此处添加了一个更复杂的示例:https://github.com/DonMag/Scratch2021
它使用垂直滚动视图中的垂直堆栈视图作为“主视图”UI,在堆栈视图中添加子视图控制器作为“组件”。