UITableView的“静态单元格”
贺雅健
2023-12-01
##UITableView的“静态单元格”
###静态单元格与动态单元格
* 静态单元格:不会随数据的改变而改变,当在storyboard中创建好后,显示的数据内容和模板样式都固定不变。需要修改,只能在storyboard中修改
* 动态单元格:设计时只是创建了一个空壳和一些基础属性(比如背景色、字体等),实际的大小和显示方式,需要在运行时通过显示数据的不同而动态计算再显示。优点是可以根据不同数据模型,实现不同的显示方式,灵活
###静态单元格与动态单元格使用场景
* 静态单元格:界面内容固定,不会发生任何变化,此时使用静态单元格创建界面比较方便
* 动态单元格:界面的内容会随着数据的变化而不断变化,就需要使用动态单元格
**使用静态单元格的注意事项**
不能通过在UIView中拖拽UITableView的方式来使用静态单元格,需要创建新的UITableViewController,并在属性中将content改为Static Cells
###静态单元格的使用步骤
1. 拖拽一个UITableViewController到storyboard中
2. 将tableView的content属性设置为 static cell
3. 删除多余静态单元格,保留一个
4. 根据是否需要分组,设置tableView为plain或者grouped样式
5. 如果是grouped样式,在左侧控制器中设置每一组的行数
6. 选中单元格,为单元格设置相关内容数据
##QQ好友列表案例
###懒加载数据
模型的嵌套
###自定义cell
* 封装类方法,快速创建UITableViewCell,并实现cell的重用
* cell采用系统默认的模板即可,不需要重写UITableViewCell的创建方法
* 重写cell的数据模型setter方法,给cell的子控件设置数据
###自定义分组的headerView
#####为什么分组headerView不能采用直接创建UIView的方式自定义模板
tableView的分组headerView,具有重用功能,实际是UITableViewHeaderFooterView,是UIView的子类。通过xib创建模板拖拽UIView,或者代码创建的UIView对象搭建headerView,无法让tableView实现headerView的重用功能。故只能新建继承自UITableViewHeaderFooterView的子类,代码创建需求的模板header
###自定义UITableViewHeaderFooterView类
* 封装类方法快速创建对象
* 父类自带的创建方法不能满足要求,重写父类的创建headerView方法,在方法中创建需要的子控件,并在类扩展中设置对应类型的子控件与其进行关联。设置子控件的基本属性
* 设置数据模型属性,重写模型属性的setter方法,在方法中为子控件设置数据
* 重写父类的 layoutSubviews 方法,设置子控件的frame
###为什么不能在重写数据模型的setter方法中通过headerView的frame来设置子控件的frame
* 在刚刚创建好的headerView中获取的headerView,在没有为其设置frame时,默认都是0
* 当程序运行时,创建headerView的方法将headerView返回后,UITableView在执行时用到headerView时,会将headerView添加到UITableView中
* 如果UITableView即将要使用headerView,就会根据一些内部设置来自动为headerView的frame赋值
* 所以,当在设置headerView的数据时,headerView并没有即将被UITableView使用,此时是获取不到headerView的frame的
* 不能在setter方法中通过headerView的frame设置子控件的frame,但是可以为子控件指定固定的frame
###重写layoutSubViews方法
当headerView即将被添加到tableView中时,tableView会计算出headerView的frame,此时就可以获取到headerView的frame了,就可以在这个方法中,根据headerView的frame设置子控件的frame
```objc
- (void)layoutSubViews{
//注意:一定要先调用父类的layoutSubViews方法,不然可能子类创建的子控件只能显示而失去功能
[super layoutSubViews];
//设置按钮的frame和headerView的frame一致
self.btnGroupTitle.frame = self.bounds;
//设置label的frame
xxxx 计算lblCount的x/y/width/height
xxxx
self.lblCount.frame = CGRectMake(lblX,lblY,lblW,lblH);
}
```
####按钮内容显示细节调整
```objc
//
设置按钮内容左对齐
btnGroupTitle.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
//
设置按钮内容的内边距
btnGroupTitle.contentEdgeInsets = UIEdgeInsetsMake(0,10,0,0);
//上左下右
//
设置按钮文字距离图片的边距
btnGroupTitle.titleEdgeInsets = UIedgeInsetsMake(0,5,0,0);
```
###实现headerView按钮的折叠展开功能
* 在数据模型中设置一个记录分组展开状态的属性
* 为headerView中的按钮注册一个点击事件
* 让tableView的数据源方法根据展开折叠状态,返回不同的单元格数(展开,则返回具体单元格数,折叠,则返回0,不显示单元格)
* 设置控制器为headerView的代理,当按钮被点击时,通过按钮的点击事件修改模型展开状态记录属性的值,然后通过代理刷新tableView
**局部刷新单一分组的代码实现**
```objc
- (void)groupHeaderViewDidClickTitleButton:(EHGroupHeaderView *)groupHeaderView{
//创建HeaderView时,将每个HeaderView的tag指定为所在分组的值
//
创建一个用来表示某个组的对象,根据需要代理的HeaderView的tag获取指定的组
NSIndexSet *idxSet = [NSIndexSet indexSetWithIndex:groupHeaderView.tag];
[self.tableView reloadSections:idxSet withRowAnimation:UITableViewRowAnimationFade];
}
```
###为什么不能在HeaderView的按钮点击事件方法中实现箭头图标的旋转
代理执行完代理方法后会刷新数据。而如果在HeaderView的按钮点击事件中设置图片的旋转,由于代理刷新数据有一定延迟,会将先旋转过图片的旧分组数据全部删除而刷新新的数据。此时,原来旋转过的图标,又会回到初始旋转角度。因此,设置图片的旋转,应该要在代理刷新数据之后再执行
###重写 didMoveToSuperview 实现图片的旋转
```objc
// 没必要刻意实现这个方法。在给头标题设置数据时,实现了箭头的旋转,即可
// 当子控件将要被加入到父控件中时,会调用这个方法
- (void)didMoveToSuperView{
// 根据展开状态属性值,调整旋转角度
if(self.group.isVisible){
self.btnGroupTitle.transform = CGAffineTransformMakeRotation(M_PI_2);
}else{
self.btnGroupTitle.transform = CGAffineTransformMakeRotation(0);
}
}
```
####图片旋转的细节完善
```objc
//设置按钮中图片的显示模式
btnGroupTitle.imageView.contentMode = UIViewContentModeCenter;
//居中
//设置保留图片超出边框的部分(不截掉超出边界的部分)
btnGroupTitle.imageView.clipsToBounds = NO;
```
####解决HeaderView重用时图片旋转滞留的方法
在数据模型的setter方法中,再执行一次 didMoveToSuperview中的代码。每次创建HeaderView设置数据时,根据其展开状态,设置按钮图标的旋转角度
##使用UITableViewController的自带单元格
####使用场景
当tabelView中的单元格格式一致,但是系统自带模板不能满足需求时,可以直接使用TabelViewController自带的单元格搭建模板,而不必新建xib文件拖拽UITableViewCell来搭建模板
**使用步骤**
* 为UITableViewController自带单元格设置一个重用ID
* 类似于xib创建单元格,在UITableViewController自带单元格中拖拽控件,设置属性
* 为UITableViewController自带单元格写一个关联类,为自带单元格设置数据模型属性,以及拖拽生成子控件属性等
* 在viewDidLoad方法中统一设置自带单元格的行高
**注意**:
* 在storyboard的单元格属性中设置单元格的行高是设置显示在storyboard中的行高,而不是实际运行时单元格的行高。统一设置单元格行高,只能在viewDidLoad中通过代码来设置
* 在tableView的数据源协议的获取单元格方法中,只需定义重用ID与控制器自带单元格ID一致,然后让tableView去缓存池中寻找即可。如果tableView没有找到,则会自行创建,无需再写新建cell的代码
* 使用自带单元格时,如果自带单元格在UITableViewController中被复制了多个,各自带单元格的重用ID必须不能重名
* 为自带单元格创建关联类时,无需再封装创建cell的方法,只需在storyboard中指定单元格的重用ID,并在数据源方法中指定以该ID进行获取重用单元格即可
####代码实现
```objc
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
// 1.获取模型数据
EHApp *model = self.apps[indexPath.row];
// 2.通过storyboard中的cell模板来加载单元格
static NSString *ID = @"app_cell";
//
使用storyboard中的cell,在调用这个方法时,会自动去缓存池中寻找是否有可重用的cell
//
如果没有,则以storyboard中的cell为模板创建一个新的cell
EHAppCell *cell = [tabelView dequeueReusableCellWithIdentifier:ID];
// 3.把模型设置给单元格
cell.app = model;
// 4.返回单元格
return cell;
}
```
###解决APPlist案例中单元格重用按钮状态滞留问题
* 在数据模型中设置一条BOOL属性记录下载按钮是否已被点击
* 在下载按钮的点击事件中,修改下载记录的值,同时设置按钮是否被禁用的状态
* 在数据模型属性的setter方法中,根据下载状态属性,重新设置下载按钮的状态
####点击cell中的下载按钮弹出下载提示实现
设置控制器为cell的代理
####下载提示标签弹出位置的问题及修正
* 问题:控制器在代理方法中创建提示label,如果将这个label加入到当前tableView中,则label的位置相对于tabelView的内容位置固定。因为tabelView继承自UIScrollView,当tabelView滚动时,弹出label仍然会出现在原内容所在的位置
* 解决办法
```objc
//将弹出label加入到当前应用的keyWindow中,而不是tabelView中
[[[UIApplication shareApplication] keyWindow] addSubview: lblMsg];
```
##KVC介绍
全称:Key-value Coding
**KVC思想**:每一个对象的每一条属性,都具有一个属性名称。将这个名称看做是该属性的key。通过这个key就可以为这个对象的这条属性赋值,同时,通过这个key,也可以获取这个对象的这条属性的值。当对象的属性是一个含有其他属性的对象时,通过点语法获取实例变量对象的属性的代码,称为KeyPath。通过keyPath,可以获取到更深层的属性
**相关方法**
```objc
//通过key获取属性值
- (instancetype)valueForKeyPath:@"keyPath";
//通过key为属性赋值
// 对于转换为OC对象的基本数据类型value,赋值时会智能转换回去再赋值
// 例如,value为NSString或者NSNumber类型的对象,会智能给对应的基本数据类型的property赋值
- (void)setValue: value forKeyPath:@"keyPath";
//将对象转换成字典,array中元素为需要获取的keyName(必须与对象的属性名严格一致)
- (NSDictionary *)dictionaryWithValueForKeys:(NSArray *)array;
//字典快速为模型属性赋值
- (void)setValuesForKeysWithDictionary:(NSDictionary *)dict;
```