Clean table view code(更加干净的tableview代码)
tableview在ios中被广泛的应用,所有有很多的代码直接或者间接地和tableview产生关系,包括数据提供和数据更新的以及控制tableview行为和选择某一行的反应的代码,下面将展示一些让tableview更加简洁更加结构紧凑的方法。
UITableViewController vs. UIViewController
苹果提供了UITableViewController来展示tableview,UITableViewController具有了很多很好用的特性供我们使用,使得我们可以从无用的重复代码中得到解脱。但是在另一方面,UITableViewController被设置为只能管理一个tableview,并且是全屏展示的,虽然在很多情况下这已经足够满足我们的需要了,但是还是会由一些情况我们会遇到,相应的我们也有解决方案。下面将会展示
Features of Table View Controllers(特性展示)
UITableViewController在首次加载的时候会进行tableview数据的加载,更加具体来说就是UITableViewController通过响应键盘通知或者其他的小的如滑动指示器和清空tableview的选择等任务来 帮助我们切换tableview的编辑模式。为了能够完成这些效果,在重写任意的view开头的方法都需要调用super的相应方法。
UITableViewController和其他的标准控制器相比有一个独特的卖点:对于苹果的"push to refresh"(滑动刷新)的实现,在当前情况下,唯一的官方文档记录的使用UIRefreshControl的方式是在UITableViewController中实现的。其实有很多方法使其他的控件也能使用这个UIRefreshControl,但是这些很有可能在下次更新的时候无法得到IOS的支持。
Limitations of Table View Controllers
UITableViewController的view通常只能是tableview。如果你决定在不久的将来想要在tableview的附近展示其他的view,如果可以成功将是非常幸运的。
如果是通过代码或者XIB来实现的界面,那么把UITableViewController转化为普通的控制器是很简单的,但是如果是通过storyboard来实现的UI那么将是很困难的,因为你不得不重新来拖拽控制器,并且把之前的控制器中的所有的东西都移动到新的控制器中。
最后,你需要为这个普通的控制器添加table view controller在转换过程中丢失的属性,这些丢失的特性大多只需要在viewWillAppear
或viewDidAppear
.方法中用一行搞定,改变tableview编辑状态的功能则需要实现一个决定tableview编辑状态属性的类方法。
在继续进行下去之前,这里其实还有一个更加简单的选择,而且需要考虑的更加的少。
Child View Controllers
除了完全替换UITableViewController之外,还可以将这个控制器添加到另外一个控制器中作为子控制器,这样的话这个UITableViewController可以继续管理tableview,而其他的事情则交给它的父控制器
- (void)addPhotoDetailsTableView { DetailsViewController *details = [[DetailsViewController alloc] init]; details.photo = self.photo; details.delegate = self; [self addChildViewController:details]; CGRect frame = self.view.bounds; frame.origin.y = 110; details.view.frame = frame; [self.view addSubview:details.view]; [details didMoveToParentViewController:self]; }
如果采用了这种方式,那么需要创建一个子控制器和父控制器之间联系的通道UITableViewController中的tableview的某一行那么这个消息需要传送到UITableViewController的父控制器,父控制器以此来决定跳转的页面,在这种情况下通常的处理是使用delegate,让父控制器遵守UITableViewController的代理来实现信息的传递
@protocol DetailsViewControllerDelegate - (void)didSelectPhotoAttributeWithKey:(NSString *)key; @end @interface PhotoViewController () <DetailsViewControllerDelegate> @end @implementation PhotoViewController // ... - (void)didSelectPhotoAttributeWithKey:(NSString *)key { DetailViewController *controller = [[DetailViewController alloc] init]; controller.key = key; [self.navigationController pushViewController:controller animated:YES]; } @end
正如你所看到的,采用这种结构的结果是虽然这样的代码分的很清楚,也具有较好的重用性,但代价是需要建立连接通道。这样的情况下有可能使问题更加复杂或者简单。
Separating Concerns
在处理tableview的问题的时候往往会将很多不同的贯穿于视图模型控制器的任务包含在其中。为了防止控制器变成这一群问题发生的地点,我们试着将不同的任务放在最适合处理的地方进行,这样使得我们的代码更加的可读更加的紧凑和可测试。
Bridging the Gap Between Model Objects and Cells(逾越model对象和cell之间的鸿沟)
有时候我们需要将需要展示在view层的数据叫出来,但是我们依然想要保持模型和视图之间的独立性,通常我们会将这个任务转交给tableview的数据源属性
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { PhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:@"PhotoCell"]; Photo *photo = [self itemAtIndexPath:indexPath]; cell.photoTitleLabel.text = photo.name; NSString* date = [self.dateFormatter stringFromDate:photo.creationDate]; cell.photoDateLabel.text = date; }
这种形式的代码由于很了解cell的设计形式,所以严重的扰乱了代码,而最好的解决方式是将这个操作放到cell类型的分类中去执行
@implementation PhotoCell (ConfigureForPhoto) - (void)configureForPhoto:(Photo *)photo { self.photoTitleLabel.text = photo.name; NSString* date = [self.dateFormatter stringFromDate:photo.creationDate]; self.photoDateLabel.text = date; } @end
这样的话我们的数据源方法变得非常的简单
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { PhotoCell *cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier]; [cell configureForPhoto:[self itemAtIndexPath:indexPath]]; return cell; }
Making Cells Reusable
有些时候我们只需要使用同一类型的cell便可以完成很多模型对象的展示,所以我们应该更进一步让我们的cell具有重用性。
首先我们在cell内部定义一个协议,需要被这个cell展示的模型需要遵守这个协议,然后我们简单的改变 cell的category中的configure方法,以此接受所有的遵守协议的对象类型,这些简单的操作让cell从任何特定的模型对象中脱离出来,让其可以应用在不同的数据类型中。
Handling Cell State Within the Cell
如果想要改变tableview的选中亮度或者选中的行为,那么可以通过实现两个代理方法来进行操作,通过这两个方法我们可以让选中的cell按照我们的设计进行反应
- (void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath { PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath]; cell.photoTitleLabel.shadowColor = [UIColor darkGrayColor]; cell.photoTitleLabel.shadowOffset = CGSizeMake(3, 3); } - (void)tableView:(UITableView *)tableView didUnhighlightRowAtIndexPath:(NSIndexPath *)indexPath { PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath]; cell.photoTitleLabel.shadowColor = nil; }
然而,实现了这两个代理方法,再次暴漏了cell的实现细节。如果我们希望cel出内存l或者重新设计cell,那么我们不得不改变代码。view的实现代码细节和代理的代码实现细节纠缠在一起,我们应该把这些逻辑移动到cell自己的类中(这才是封装的思想,谁的东西就放在谁里面)
@implementation PhotoCell // ... - (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated { [super setHighlighted:highlighted animated:animated]; if (highlighted) { self.photoTitleLabel.shadowColor = [UIColor darkGrayColor]; self.photoTitleLabel.shadowOffset = CGSizeMake(3, 3); } else { self.photoTitleLabel.shadowColor = nil; } } @end
一般来说,我们尽量将view层的代码具体实现细节和controller的代码具体实现细节分离开来。一个代理需要知道一个view的集中不同状态,但是
它不应该知道如何改变view树或者怎么设置属性能够得到子控件的一些状态值,所有的这些逻辑都应该屏蔽在view自己内部,然后对外提供一个接口就可以了。
Handling Multiple Cell Types(多种类型的cell类型)
如果你的tableview中又多种类型的cell,那么数据源方法就会失控了。这里的一种解决方式如下
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSString *key = self.keys[(NSUInteger) indexPath.row]; id value = [self.photo valueForKey:key]; UITableViewCell *cell; if ([key isEqual:PhotoRatingKey]) { cell = [self cellForRating:value indexPath:indexPath]; } else { cell = [self detailCellForKey:key value:value]; } return cell; } - (RatingCell *)cellForRating:(NSNumber *)rating indexPath:(NSIndexPath *)indexPath { // ... } - (UITableViewCell *)detailCellForKey:(NSString *)key value:(id)value { // ... }
其实很容易理解,根据具体的行掉用不同的方法来返回响应类型的cell就可以了
Table View Editing
tableview提供了很好用的特性,可以方便的记录和删除cell,在所有的事件中,tableview的数据源通过代理方法得到通知,但是我们却经常在这些方法中发现一些改变数据的域逻辑。
改变数据这项任务很明显是属于模型层的,而这些模型应该暴漏一些保存和删除数据的接口给外界使用,这些接口可以通过数据源方法调用,这样的话控制器就扮演了view和model之间的桥梁,但是却并不知道model的内部方法的具体实现细节,这样做的另外一个好处是,模型逻辑变得更加的容易进行测试,因为model没有再和控制器的其他任务交织在一起,变得更加纯洁了。
Conclusion
所有的控制器都应该扮演的时model和view之间交流的桥梁,但不能和特定属于model或者view的功能交织在一起,如果你能将这个原则牢记在新的话,那么代理和数据源方法将会变得更加的轻量级和简单。
这样做不仅仅有效降低了控制器的大小和复杂度,而且将业务逻辑放在了最适合处理的地方进行处理,具体的实现细节被屏蔽起来了,而控制器面对的只是简单的API,这样最终的结果是代码变得更加的容易理解并且可以很方便的进行团队开发。