这篇笔记主要记录了完成一个自适应高度的TableView的例子。例子来自https://www.raywenderlich.com/87975/dynamic-table-view-cell-height-ios-8-swift,由于原文用的是swift,与最新版的语法有所不同,我把这个例子用Objective-C改写了。demo的代码地址:
https://github.com/shishujuan/ios_study/tree/master/tableview/TableViewDemo.
1 概览
这个例子主要完成了一个表格视图的展示,其中每个单元格内容可能不同,高度也可能不一样,需要动态适应。这个demo做的事情就是从一个RSS地址拉取数据,然后在表格展示。主要知识点有AutoLayout
,UITableView
以及CocoaPods
的使用。AutoLayout前一篇文章已经有分析过,UITableView是本文的重点,后面会介绍。
而CocoaPods新出现的一个东东,它是iOS开发中一个第三方库的依赖管理工具,类似于java中的mvn和nodejs中的npm等。详细信息可以参见唐巧大大的文章介绍[用CocoaPods做iOS程序的依赖管理。我这个demo中用到的Podfile内容如下,可以看到这个demo中需要用到AFNetworkActivityLogger
, MediaRSSParser
,MBProgressHUD
这三个库。AFNetworkActivityLogger是AFNetworking2.0的一个扩展,是开发中很常用的第三方库,封装了一些网络请求,可以大幅简化我们的代码。而MediaRSSParser是这个demo才用到,用于解析RSS文件。MBProgressHUD也是很常用的一个库,用于提示进度,类似好比我们自己手写的那些ActivityIndicator,当然比我们手写要方便很多。
我们只需要在工程目录下运行pod install
,就会下载好第三方库了。如果运行命令卡住了的话,可以用这个命令pod install --verbose --no-repo-update
.
platform :ios, '8.0'
pod 'AFNetworkActivityLogger', '~> 2.0'
pod 'MediaRSSParser', '~> 1.0'
pod 'MBProgressHUD', '~> 0.9'
2 创建Scene
本demo创建了3个Scene(除去导航栏),Storyboard界面如下:
其中Deviant Art为列表页面,Deviant Article为不带图片的详情页,而Deviant Media则为带图片的详情页。其中:
- Devian Art顶部有一个嵌入的
Navigation Bar
(导航栏,边上有一个刷新按钮用于重新加载数据),下面有一个Text Field
控件的搜索框,搜索框下面是一个Table View
,用于展示作品列表。注意,我们这里还没有添加Table View Cell
,稍后我们会看到怎么自定义这个Cell。 - Devian Article为不含图片的作品详情页。这个场景只有两个Label,分别是标题和副标题。
- Devian Media为带图片的作品详情页。这个场景除了标题和副标题两个Label外,还有一个Image View。
3 解析RSS
解析RSS借助了第三方库RSSParser来实现。代码如下:
(void)parseForQuery:(NSString *)query {
[self showProgressHUD];
RSSParser *parser = [[RSSParser alloc] init];
[parser parseRSSFeed:deviantArtBaseStringUrlString parameters:[self parametersForQuery:query] success:^(RSSChannel *channel) {
[self convertItemPropertiesToPlainText:channel.items];
self.items = channel.items;
[self hideProgressHUD];
[self reloadTableViewContent];
} failure:^(NSError *error) {
[self hideProgressHUD];
NSLog(@"Error:%@", error);
}];
}
- (void)showProgressHUD {
MBProgressHUD *hud = [MBProgressHUD showHUDAddedTo:self.view animated:true];
hud.labelText = @"Loading";
}
- (void)hideProgressHUD {
[MBProgressHUD hideHUDForView:self.view animated:true];
}
其中showProgressHUD和hideProgressHUD函数用到了第三方库MBProgressHUD来实现,省去了我们自己去添加Activity Indicator
。解析RSS获取到了作品列表,我们需要展示在Table View中,因此接下来我们就要添加自定义的Table View Cell。
4 创建Basic Cell
添加一个Table View Cell到Table View中,并在属性标签中设置Class为BasicCell,设置初始的rowHeight为83。其中内容为两个Label(Title Label字体大小为17,Subtitle Label为15,包含图片的Cell我们在后面再添加),如下所示:
约束关系如下所示:
然后设置两个Label的抗压缩和抗拉伸优先级。这里设置Title Label的抗压缩和抗拉伸优先级为751,高于Subtitle Label的750,也就是说要优先满足Title Label的约束:
5 配置Table View
为了动态设置Cell的高度,先要配置下Table View的几个属性。如下所示,设置了估算高度以及rowHeight属性,此外,设置Table View的delegate和dataSource为View Controller自身。注意,需要设置View Controller的automaticallyAdjustsScrollViewInsets为NO,否则会看到Table View与Text Field之间有一段空白。
- (void)configureTableView {
//设置rowHeight为AutomaticDimension是为了让Table View通过Auto Layout的约束去定义每个Cell的高度。
self.feedTableView.rowHeight = UITableViewAutomaticDimension;
//设置estimatedRowHeight为了提高Table View的渲染效率,这是一个估算高度。
self.feedTableView.estimatedRowHeight = 160.0;
self.feedTableView.delegate = self;
self.feedTableView.dataSource = self;
self.searchTextField.delegate = self;
//注意,不设置这个会导致Table View与searchTextField有一段空白。
self.automaticallyAdjustsScrollViewInsets = NO;
}
接下来实现dataSource和deletegate的方法。
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.items count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self basicCellAtIndexPath:indexPath];
}
- (BasicCell *)basicCellAtIndexPath:(NSIndexPath *)indexPath {
BasicCell *cell = [self.feedTableView dequeueReusableCellWithIdentifier:basicCellIdentifier];
[self setTitleForCell:cell indexPath:indexPath];
[self setSubtitleForCell:cell indexPath:indexPath];
return cell;
}
- (void)setTitleForCell:(BasicCell *)cell indexPath:(NSIndexPath *)indexPath {
RSSItem *item = self.items[indexPath.row];
cell.titleLabel.text = item.title ? item.title : @"[No Title]";
}
- (void)setSubtitleForCell:(BasicCell *)cell indexPath:(NSIndexPath *)indexPath {
RSSItem *item = self.items[indexPath.row];
NSString *subTitleText = item.mediaText ? item.mediaText : item.mediaDescription;
if (subTitleText) {
subTitleText = subTitleText.length > 200 ? [subTitleText substringToIndex:200] : subTitleText;
} else {
subTitleText = @"";
}
cell.subtitleLabel.text = subTitleText;
}
6 添加Image Cell
为了显示图片,需要新增加一个Table View Cell,这里设置标识为Image Cell
。与Basic Cell不同的是,需要增加一个Image View用来显示图片。添加的约束如下所示,具体参见例子代码:
这里有几个地方要注意一下:
- Image View的宽度和高度都设置的100pt,注意,这两个约束的优先级这里设置为999。此外,Image View与Cell底部的距离>=20这个约束的优先级也设置为999。
- Subtitle Label与Cell底部的距离 >= 20 这个约束的优先级为1000。也就是说要优先满足Subtitle Label的约束。
接下来需要设置Image Cell,相关代码如下:
//修改cellForRowAtIndexPath方法,区分有图片还是没图片分开处理。
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
if ([self hasImageAtIndexPath:indexPath]) {
return [self imageCellAtIndexPath:indexPath];
} else {
return [self basicCellAtIndexPath:indexPath];
}
}
- (BOOL)hasImageAtIndexPath:(NSIndexPath *)indexPath {
RSSItem *item = self.items[indexPath.row];
NSArray<RSSMediaThumbnail *> *mediaThumbnailArray = [item mediaThumbnails];
for (RSSMediaThumbnail *mediaThumbnail in mediaThumbnailArray) {
if (mediaThumbnail.url != nil) {
return YES;
}
}
return NO;
}
- (ImageCell *)imageCellAtIndexPath:(NSIndexPath *)indexPath {
//在Storyboard中已经设置了ImageCell标识,
//所以这里是肯定可以取得cell的,可以省去是否为nil的判断。
ImageCell *cell = [self.feedTableView dequeueReusableCellWithIdentifier:imageCellIdentifier];
[self setImageForCell:cell indexPath:indexPath];
[self setTitleForCell:cell indexPath:indexPath];
[self setSubtitleForCell:cell indexPath:indexPath];
return cell;
}
- (void)setImageForCell:(ImageCell *)cell indexPath:(NSIndexPath *)indexPath {
RSSItem *item = self.items[indexPath.row];
RSSMediaThumbnail *mediaThumbnail;
if (item.mediaThumbnails.count >= 2) {
mediaThumbnail = item.mediaThumbnails[1];
} else {
mediaThumbnail = item.mediaThumbnails[0];
}
//预设图片为nil,防止之前的图片重用导致看起来图片错乱
cell.customImageView.image = nil;
if (mediaThumbnail.url != nil) {
[cell.customImageView setImageWithURL:mediaThumbnail.url];
}
}
7 其他
另外需要设置Basic Cell和Image Cell到对应作品详情视图的segue,这个具体参见代码。另外,对Table View的Cell高度的估算也在最终代码有优化。最终运行效果如下图所示: