iOS开发基础12-深入理解UITableView(一)

在 iOS 应用开发中,UITableView 是展示列表数据的常用控件。本文将详细介绍 UITableView 的工作原理、数据展示、Cell 的重用机制、自定义 Cell 以及如何通过 UITableView 展示数据。

一、什么是 UITableView

在 iOS 中,UITableView 是用来展示列表数据的主要控件。它继承自 UIScrollView,因此天然支持垂直滚动,同时具备极佳的性能。

UITableView 的两种样式

  • UITableViewStylePlain:标准的列表样式,所有单元格都在一个连续的平面上。
  • UITableViewStyleGrouped:分组样式,每个组有独立的分隔符或背景。

二、如何展示数据

数据源(DataSource)

UITableView 通过一个数据源对象来获取数据。数据源对象必须遵循 UITableViewDataSource 协议,这样 UITableView 才能获取数据进行展示。

数据展示的过程如下:

  1. 调用数据源方法 numberOfSectionsInTableView: 获取有多少个分组。
  2. 调用数据源方法 tableView:numberOfRowsInSection: 获取每个分组中的行数。
  3. 调用数据源方法 tableView:cellForRowAtIndexPath: 获取每一行的内容。

示例代码

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    // 返回分组数量
    return [self.dataArray count];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    // 返回每个分组中的行数
    return [[self.dataArray objectAtIndex:section] count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    // 获取某一行的数据并返回对应的Cell
    static NSString *cellIdentifier = @"cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
    }
    // 配置cell内容
    cell.textLabel.text = [NSString stringWithFormat:@"Section %ld Row %ld", (long)indexPath.section, (long)indexPath.row];
    return cell;
}

三、Cell简介

UITableView 的每一行都是一个 UITableViewCell,通过数据源中的 tableView:cellForRowAtIndexPath: 方法来创建和初始化每一行。

UITableViewCell 的辅助指示视图

每个 UITableViewCell 都有一个 contentView,这是用于显示内容的父视图。你可以通过 accessoryType 属性来设置辅助图标,包括:

  • UITableViewCellAccessoryNone:不显示图标。
  • UITableViewCellAccessoryDisclosureIndicator:显示箭头(通常表示可以点击查看详细信息)。
  • UITableViewCellAccessoryDetailButton:显示信息按钮。
  • UITableViewCellAccessoryDetailDisclosureButton:显示详细信息按钮和箭头。
  • UITableViewCellAccessoryCheckmark:显示对勾。

还可以通过 accessoryView 属性来自定义辅助指示视图。

ContentView 子视图

contentView 下默认有三个子视图:

  • 两个 UILabel,可以通过 textLabeldetailTextLabel 属性访问。
  • 一个 UIImageView,可以通过 imageView 属性访问。

可以通过 UITableViewCellStyle 属性来决定子视图的布局和样式:

  • UITableViewCellStyleDefault
  • UITableViewCellStyleSubtitle
  • UITableViewCellStyleValue1
  • UITableViewCellStyleValue2

四、Cell的重用原理

由于内存限制,创建大量 UITableViewCell 会耗尽设备内存。为了提升性能,UITableView 提供了 Cell 重用机制。

重用原理

当列表滚动时,移出屏幕的 UITableViewCell 会放入重用池中。调用 dequeueReusableCellWithIdentifier: 方法可以从重用池中取出已存在的 Cell,这样可以避免重复创建,节省资源。

重用代码示例

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cellIdentifier = @"cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
    }
    // 配置cell内容
    cell.textLabel.text = [NSString stringWithFormat:@"Section %ld Row %ld", (long)indexPath.section, (long)indexPath.row];
    return cell;
}

不同类型的 Cell 重用

如果一个 UITableView 中有不同类型的 Cell,可以通过设置不同的 reuseIdentifier 来区分每种类型的 Cell,从而避免类型错误。

五、通过代码自定义 Cell

当需要自定义 UITableViewCell 时,可以通过继承 UITableViewCell 来实现个性化展示。

自定义 Cell 的步骤

  1. 新建一个继承自 UITableViewCell 的类。
  2. 重写 initWithStyle:reuseIdentifier: 方法,添加需要显示的子控件。
  3. 提供两个模型:数据模型和框架模型。
  4. Cell 拥有一个框架模型,重写框架模型的 setter 方法,在该方法中设置子控件的数据和框架。

示例代码

// 自定义Cell的头文件
@interface CustomTableViewCell : UITableViewCell
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIImageView *customImageView;
@end

// 自定义Cell的实现文件
@implementation CustomTableViewCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        self.titleLabel = [[UILabel alloc] init];
        [self.contentView addSubview:self.titleLabel];
        
        self.customImageView = [[UIImageView alloc] init];
        [self.contentView addSubview:self.customImageView];
    }
    return self;
}

- (void)layoutSubviews {
    [super layoutSubviews];
    // 设置子控件的 frame
    self.titleLabel.frame = CGRectMake(10, 10, 200, 30);
    self.customImageView.frame = CGRectMake(self.contentView.frame.size.width - 40, 10, 30, 30);
}

@end

// 使用自定义Cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *customCellIdentifier = @"customCell";
    CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:customCellIdentifier];
    if (!cell) {
        cell = [[CustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:customCellIdentifier];
    }
    cell.titleLabel.text = @"Custom Cell";
    cell.customImageView.image = [UIImage imageNamed:@"example"];
    return cell;
}

六、利用 UITableView 展示汽车数据案例

通过一个具体的示例来展示如何使用 UITableView 展示复杂数据。

示例代码

#import "ViewController.h"
#import "CarGroup.h"
#import "Car.h"

#define cellIdentifier @"carCell"
@interface ViewController () <UITableViewDataSource, UITableViewDelegate>
@property (nonatomic, copy) NSArray *carGroups;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 设置表格的样式和数据源
    self.tableView.dataSource = self;
    self.tableView.delegate = self;
    
    // 设置右侧索引文字的颜色和背景色
    self.tableView.sectionIndexColor = [UIColor redColor];
    self.tableView.sectionIndexBackgroundColor = [UIColor blackColor];
    
    // 注册Cell
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:cellIdentifier];
}

- (NSArray *)carGroups {
    if (!_carGroups) {
        NSString *path = [[NSBundle mainBundle] pathForResource:@"cars" ofType:@"plist"];
        NSArray *carGroupsDictArray = [NSArray arrayWithContentsOfFile:path];
        
        NSMutableArray *carGroupsM = [NSMutableArray array];
        for (NSDictionary *dict in carGroupsDictArray) {
            CarGroup *group = [CarGroup carGroupWithDict:dict];
            [carGroupsM addObject:group];
        }
        _carGroups = carGroupsM;
    }
    return _carGroups;
}

#pragma mark - UITableViewDataSource
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return self.carGroups.count;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    CarGroup *group = self.carGroups[section];
    return group.cars.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier forIndexPath:indexPath];
    CarGroup *group = self.carGroups[indexPath.section];
    Car *car = group.cars[indexPath.row];
    
    cell.textLabel.text = car.name;
    cell.imageView.image = [UIImage imageNamed:car.icon];
    
    return cell;
}

#pragma mark - UITableViewDelegate
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    CarGroup *group = self.carGroups[section];
    return group.title;
}

- (NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView {
    return [self.carGroups valueForKeyPath:@"title"];
}

@end

// CarGroup.h
@interface CarGroup : NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic, strong) NSArray *cars;
+ (instancetype)carGroupWithDict:(NSDictionary *)dict;
@end

// CarGroup.m
#import "CarGroup.h"
#import "Car.h"

@implementation CarGroup
+ (instancetype)carGroupWithDict:(NSDictionary *)dict {
    CarGroup *group = [[self alloc] init];
    group.title = dict[@"title"];
    
    NSMutableArray *cars = [NSMutableArray array];
    for (NSDictionary *carDict in dict[@"cars"]) {
        Car *car = [Car carWithDict:carDict];
        [cars addObject:car];
    }
    group.cars = cars;
    
    return group;
}
@end

// Car.h
@interface Car : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *icon;
+ (instancetype)carWithDict:(NSDictionary *)dict;
@end

// Car.m
#import "Car.h"

@implementation Car
+ (instancetype)carWithDict:(NSDictionary *)dict {
    Car *car = [[self alloc] init];
    [car setValuesForKeysWithDictionary:dict];
    return car;
}
@end

底层逻辑分析

在这个示例中,我们从 plist 文件中读取汽车数据,然后通过模型类 CarGroupCar 来组织这些数据。通过重写 UITableViewDataSourceUITableViewDelegate 方法,我们将数据展示在 UITableView 上。

  • 数据模型:通过模型类将原始数据转化为可操作的数据对象,方便后续的操作和展示。
  • 数据展示:通过实现 UITableViewDataSourceUITableViewDelegate 方法,将数据源和视图控制器连接起来,完成数据的展示和交互操作。

七、Cell不等高

UITableView 中,如果每个 UITableViewCell 的高度都相同,可以显著简化实现逻辑。然而,有时候我们需要实现每个 Cell 高度不一样的情况。下面介绍几种实现不等高 UITableView 的做法。

方法一:实现 tableView:heightForRowAtIndexPath:

这是最常用的方法之一,通过实现 UITableViewDelegate 协议中的 tableView:heightForRowAtIndexPath: 方法,可以设置每个 Cell 的高度。

示例代码

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    // 根据 indexPath 返回对应的 Cell 高度
    if (indexPath.row % 2 == 0) {
        return 60.0; // 偶数行高度为60
    } else {
        return 100.0; // 奇数行高度为100
    }
}

逻辑分析

这个方法允许针对每个 Cell 直接返回不同的高度。实现简单且直观,但如果需要计算复杂的高度还需要一些额外的准备工作,例如提前计算好每个 Cell 需要的高度。

方法二:使用自动布局和估算高度

自 iOS 8 开始,UITableView 支持自动布局(Auto Layout)计算 Cell 高度,同时利用估算高度(estimatedRowHeight)来提升性能。

  1. 启用自动布局
  2. 设置估算高度

示例代码

- (void)viewDidLoad {
    [super viewDidLoad];

    // 启用自动布局
    self.tableView.estimatedRowHeight = 80.0;
    self.tableView.rowHeight = UITableViewAutomaticDimension;
}

在 Cell 配置中,确保所有子视图都有明确的 Auto Layout 约束,以支持自动高度计算。

配置 Cell

在自定义的 UITableViewCell 中,采用自动布局配置子视图。例如:

// CustomTableViewCell.h
@interface CustomTableViewCell : UITableViewCell
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UILabel *detailLabel;
@end

// CustomTableViewCell.m
@implementation CustomTableViewCell

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        self.titleLabel = [[UILabel alloc] init];
        self.titleLabel.numberOfLines = 0; // 多行显示
        [self.contentView addSubview:self.titleLabel];

        self.detailLabel = [[UILabel alloc] init];
        self.detailLabel.numberOfLines = 0; // 多行显示
        [self.contentView addSubview:self.detailLabel];
        
        // 添加约束
        self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
        self.detailLabel.translatesAutoresizingMaskIntoConstraints = NO;
        
        [NSLayoutConstraint activateConstraints:@[
            [self.titleLabel.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:10],
            [self.titleLabel.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:10],
            [self.titleLabel.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor constant:-10],

            [self.detailLabel.topAnchor constraintEqualToAnchor:self.titleLabel.bottomAnchor constant:10],
            [self.detailLabel.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:10],
            [self.detailLabel.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor constant:-10],
            [self.detailLabel.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor constant:-10]
        ]];
    }
    return self;
}
@end

逻辑分析

  • 自动布局:利用自动布局,系统会根据设置好的约束动态计算 Cell 的高度。
  • 估算高度:设置估算高度可以让 UITableView 提前计算好部分内容的高度,从而提高滚动性能和加载速度。

方法三:预计算高度并缓存

如果计算不同 Cell 的高度比较复杂,可能导致滚动卡顿问题。此时,可以预先计算好每个 Cell 的高度并进行缓存。

示例代码

  1. 定义高度缓存
@property (nonatomic, strong) NSMutableDictionary *heightCache;
@property (nonatomic, strong) NSArray *dataArray; // 假设数据源是这个
  1. tableView:heightForRowAtIndexPath: 方法中使用缓存
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *cachedHeight = self.heightCache[@(indexPath.row)];
    if (cachedHeight) {
        return cachedHeight.floatValue;
    } else {
        CGFloat calculatedHeight = [self calculateHeightForIndexPath:indexPath];
        self.heightCache[@(indexPath.row)] = @(calculatedHeight);
        return calculatedHeight;
    }
}
  1. 计算高度

calculateHeightForIndexPath: 方法中实现复杂的高度计算逻辑,例如根据文本内容来计算高度:

- (CGFloat)calculateHeightForIndexPath:(NSIndexPath *)indexPath {
    // 根据文本内容和预制的字体样式计算文本所需的高度
    NSString *text = self.dataArray[indexPath.row];
    CGSize maxSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, CGFLOAT_MAX); // 最大宽度限制
    CGRect textRect = [text boundingRectWithSize:maxSize
                                         options:NSStringDrawingUsesLineFragmentOrigin
                                      attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:17]}
                                         context:nil];
    return textRect.size.height + 20.0; // 增加一些内边距
}

逻辑分析

  • 高度计算:提前计算好并缓存高度可以显著提升 UITableView 的性能,避免频繁的高度计算。
  • 缓存机制:通过字典缓存每行的高度,减少重复计算。

结论

上述方法从多角度解决了 UITableView 中不等高 Cell 的实现。选择合适的方法取决于具体的应用场景和性能需求:

  1. 简单情景:直接实现 tableView:heightForRowAtIndexPath:
  2. 多样化内容且支持自动布局:使用 UITableViewAutomaticDimension 和估算高度。
  3. 高度计算复杂且需要优化性能:提前计算并缓存 Cell 高度。

本文详细介绍了 UITableView 的基本概念、如何展示数据、Cell 的基本使用和高级自定义、以及 Cell 重用机制。通过一个具体的案例展示了如何在 UITableView 中展示数据。

掌握了这些知识点之后,你将能更加高效地使用 UITableView 来构建数据驱动的 iOS 应用。

posted @ 2015-07-19 22:52  Mr.陳  阅读(1396)  评论(0编辑  收藏  举报