1 代理方法的生命周期
- 如果不设置
estimatedRowHeight
(属性或代理方法),会去获取所有数据源的cell高度(包括屏幕外,这可能涉及不必要的计算) - 设置
estimatedRowHeight
后,只获取部分cell的实际高度。 - begin/endUpdates也不是刷新整个已加载的cell列表。
1.1 注意事项
- 重写cell的setFrame方法 外部决定内部,不是托管布局的办法。
- 手动算高度时,需要注意 在cell assemble方法中算好,而不是在heightForRow中用数据源计算。
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView cellForRowAtIndex:indexPath]; //会引发死循环,见第一张图
}
复制代码
1.2 Cell结构
- subview添加到cell.contentView上。
- 高度,如果cell的style不是UITableViewCellSeparatorStyleNone,那么cell.contentView的高度比cell的高度少1
- 背景色,如果设置cell.contentView的背景色,移动后的空白会显示cell的背景色。使用cell设置时左移或者右移颜色是不会变的。
initWithStyle: reuseIdentifier:
designated初始化方法
2 一些应用
2.1 Cell中嵌套WebView
如果cell高度和webview有关,需要在加载完更新高度的话
- 注意避免死循环。(1.1节中第2条注意事项)
- 不要复用cell,因为加载时间太长,如果复用了一个很长的cell,又加载很慢,会有一个长长的空白,而且还可能抖动严重。
2.2 Header中嵌套WebView
如:新闻详情页下的推荐文件。 最简单的做法,在Header中放webView,tableView会自己处理Header的高度
。 webView加载完成后,修改webView的frame,然后刷新tableview的高度。
另一种做法较繁琐,体验也不好。 思路值得借鉴,通过设置ContentInset,让WebView作为subView来处理。
//webview
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
// 一开始是webview在滚动,tablview并没有滚动。
// 直到webivew到底,其scrollview滑不动了。
// 此时,tableview的scrollview才接管滑动(tableview在webview下面,所以后接收手势)
// 此时如果下滑,因为禁止了webview的scrollview滑动,所以滑动的是tablview的scrollview。
if(self.tableView.contentOffset.y==0) {
[self.webView.scrollView setScrollEnabled:YES];
}else {
[self.webView.scrollView setScrollEnabled:NO];
}
}
//tableview
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
if(self.tableView.contentOffset.y==0) {
[self.webView.scrollView setScrollEnabled:YES];
}else {
[self.webView.scrollView setScrollEnabled:NO];
}
}
复制代码
2.3 TableView嵌套TableView,朋友圈的评论
- 合理布局
- avatar,name,content,展开/折叠 -> 一个view
- 图片,share的文章/视频/音乐 -> 一个view
- 点赞、分享、评论 -> 一个view
- 根据数据源提前算好cell及三层view的高度
- 因为层级较深,用ViewModel非常合适 比如用户评论时:1 需要弹起键盘,2 需要修改数据源。 这两个操作交给controller,都太麻烦了。 1使用
单例键盘对象
,2类似于MVVM,cell持有自己的数据源、tableview
。
2.4 section header悬停
- UITableHeaderView不会悬停。
- sectionHeader悬停与否,取决于
UITableViewStyle
。- Plain,会在窗口顶端悬停,不随着滑动继续上移。
- Grouped,不悬停,随着滑动继续上移。
//delegate for headerInSection
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
//返回的view的frame,决定不了真实高度。
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
//高度由这决定
}
//style是readonly属性,创建时决定。
//如果是tableviewcontroller,则需要使用xib文件修改。
复制代码
2.5 Cell需要VC的资源
场景:复杂Cell,层级较深,需要利用VC的资源时。 Cell的内部响应托管给VC。如:头条删除新闻要求选原因的弹框。
- 通过递归nextResponder拿到VC,然后用target:action:绑定到
cell的方法
。 - 也可以用RAC,注意因为复用,每个Cell只需要绑定一次。
2.6 Cell需要刷新TableView高度
如:微博评论展开、微信增加评论等
- 通过weak持有tablview
- 也可以通过RAC?
2.7 聊天窗口 & 键盘输入时
修改tableview的bounds。
2.8 scrollToBottom
微信聊天窗口,点文本框,对话栏不论contentOffset都滚动到底部 主要是contentSize和bounds。
- (void)tableViewScrollsToBottom {
if(self.chatDataArray.count<1){
return;
}
//如果不到一屏,就不拖到最下
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:self.chatDataArray.count-1 inSection:0]];
CGFloat lastCellMaxY = CGRectGetMaxY(cell.frame);
if(cell!=nil && lastCellMaxY<self.tableView.frame.size.height) {
return;
}
CGFloat yOffset = 0;
//如果tableview的contentSize超过table的bounds高度,就向上滚动
if (self.tableView.contentSize.height > self.tableView.bounds.size.height) {
yOffset = self.tableView.contentSize.height - self.tableView.bounds.size.height;
}
[self.tableView setContentOffset:CGPointMake(0, yOffset) animated:NO];
// SHWCRLog([NSString stringWithFormat:@"after tableView.contentSize %f, contentOffset %f", self.tableView.contentSize.height, self.tableView.contentOffset.y]);
}
复制代码
2.9 曝光统计
//TableView
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
AModel *model = self.datas[indexPath.row];
[cell start_show:model.uiModel];
}
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath {
[cell stop_show];
}
//Cell
- (void)start_show:(AModel*)model {
NSTimer *timer = [NSTimer timerWithTimeInterval:3
target:self
selector:@selector(firedTimer:)
userInfo:model repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
//避免cell复用的问题,使用队列来管理timer。
@synchronized(self.timerArray) {
[self.timerArray addObject:timer];
}
}
- (void)stop_show {
@synchronized(self.timerArray) {
if(self.timerArray.firstObject) {
NSTimer *timer = self.timerArray.firstObject;
[timer invalidate];
[self.timerArray removeObjectAtIndex:0];
}
}
}
- (void)firedTimer:(NSTimer *)timer {
@synchronized(self.timerArray) {
[self.timerArray removeObject:timer];
}
... //埋点
}
复制代码
2.10 TableView加Header、Footer后与MJRefresh的冲突】
网上查了下,说是因为iOS11中对自动高度的默认值改变造成的。可以通过关闭高度自动设置修复。 iOS10之后,可以用自带的refreshControl,iOS开发之UIRefreshControl使用踩坑
3 Cell高度方案,如无性能问题,委托给tableview
- tableview自己估算,其实只是为了计算contentSize。
- tableview会根据autolayout计算cell的实际高度
- 如果subview非常复杂,卡顿了,自己做对model进行高度缓存。
3.1 原理
Tableview是一个ScrollView,需要知道contentSize,才能有合理ScrollView的预估。 那么,Tableview是如何估计contentSize的呢?
- 1 所有cell都具有固定高度,设置rowHeight属性
tableView.rowHeight = 88;
tableView.rowHeight = UITableViewAutomaticDimension;
复制代码
此时,contentSize= cell高度*数据源个数 + header高度等
- 2 cell高度不固定,设置代理方法,此时rowHeight属性失效。
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// return xxx
}
复制代码
在没有设置estimatedRowHeight时,就会询问每个cell的高度。
3.2 不需要手动计算高度,只需要缓存高度。
在cell的layoutSubviews
布局完成后,拿到cell的高度(Masonry的话,要加一句[self layoutIfNeeded]
)。
4 优化
预估高度,延后组装cell,异步解码。
- 延后cell子控件组装和初始化
- 在setModel中
dispatch_once
来实现组件初始化。 - 网上还有一种方式是,每次dequeue后清理cell的contentView的子控件,以节省内存,如果这样实现,setModel中取完成子控件组装就不能是dispatch_once的。个人不建议这样做,原因是,增加了内存分配和回收,性能上不划算。
- 在setModel中
- 异步解码
- 利用RunLoop空闲时间,组装cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:IDENTIFIER];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
//不直接加载图片,将加载图片的代码交给RunLoop
[self addTask:^BOOL{
[ViewController addImageWith:cell];
return YES;
} withKey:indexPath];
return cell;
}
//MARK: 添加任务, cellForRowAtIndex时,调用
-(void)addTask:(RunloopBlock)unit withKey:(id)key{
[self.tasks addObject:unit];
[self.tasksKeys addObject:key];
//保证之前没有显示出来的任务, 不再浪费时间加载
if (self.tasks.count > self.max) {
[self.tasks removeObjectAtIndex:0];
[self.tasksKeys removeObjectAtIndex:0];
}
}
//MARK: 回调函数, 一次RunLoop来一次
static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
ViewController * vc = (__bridge ViewController *)(info);
if (![vc isKindOfClass:[ViewController class] || vc.tasks.count == 0) {
return;
}
BOOL result = NO;
while (result == NO && vc.tasks.count) {
//取出任务
RunloopBlock unit = vc.tasks.firstObject;
//执行任务
result = unit();
//干掉第一个任务
[vc.tasks removeObjectAtIndex:0];
//干掉标示
[vc.tasksKeys removeObjectAtIndex:0];
}
}
//添加观察者,在viewDidLoad中调用
-(void)addRunloopObserver{
//获取当前RunLoop
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//创建centext
CFRunLoopObserverContext context = {
0,
( __bridge void *)(self),
&CFRetain,
&CFRelease,
NULL
};
//声明观察者
static CFRunLoopObserverRef defaultModeObsever;
//创建观察者
defaultModeObsever = CFRunLoopObserverCreate(NULL,
kCFRunLoopBeforeWaiting,
YES,
NSIntegerMax - 999,
&Callback,
&context
);
//添加观察者到RunLoop中。需要在dealloc中remove掉这个观察者
CFRunLoopAddObserver(runloop, defaultModeObsever, kCFRunLoopDefaultMode);
//c语言创建的对象,release此处的引用
CFRelease(defaultModeObsever);
}
复制代码
常用API
1 属性
_tableView.bounces = NO;
_tableView.estimatedRowHeight = 152;
_tableView.rowHeight = UITableViewAutomaticDimension;
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
复制代码
2 只刷新高度
[self.tableView beginUpdates];
[self.tableView endUpdates];
复制代码
3 去掉分割线
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
复制代码
4 全局刷新、局部刷新
- 全局刷新: [self.tableView reloadData];
- 修改局部刷新, reloadRowsAtIndexPaths
- 删除局部刷新,deleteRowsAtIndexPaths
5 屏幕显示的cell
- (NSArray*)visibleCells;
- (NSArray*)indexPathsForVisibleRows;
复制代码
初始化代码
//复杂Cell,根据需求看是否持有VC、TableView、VM
@protocol SomeCellDelegate
- (void)onDelClicked:(id)sender; //cell的内部响应托管给VC。如:头条删除新闻要求选原因的弹框。
@end
@property (nonatomic, strong)SomeModel *model;
@property (nonatomic, weak)UIViewController<SomeCellDelegate> * cellDelegate;
@property (nonatomic, weak)UITableView * tableView; //如:cell高度变化,要求TableView刷新高度
//TableViewCell
- (void)awakeFromNib {
[super awakeFromNib];
[self setUp];
}
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if(self) {
[self setUp];
}
return self;
}
- (void)setUp {
self.backgroundColor = [UIColor colorWithHexString:[SHWRUMetaHolder inst].feed.bgColor alpha:1];
[self.contentView addSubview:self.titleLabel];
[self.contentView addSubview:self.contentImageView];
[self.contentView addSubview:self.separateLine];
}
- (void)setModel:(SHWRUNewsWrappedModel *)model {
_model = model;
[self updateContent];
[self updateLayout];
}
- (void)updateLayout {
}
- (void)updateContent {
}
复制代码