通过实现一个TableView来理解iOS UI编程

项目代码可以从GitHUb上获得:https://github.com/yishuiliunian/DZTableView

先说点题外话。我们在日常做和IOS的UI相关的工作的时候,有一个组件的使用频率非常高–UITabelView。于是就要求我们对UITableView的每一个函数接口,每一个属性都了如指掌,只有这样在使用UITableView的时候,我们才能游刃有余的处理各种需求。不然做出来的东西,很多时候只是功能实现了,但是程序效率和代码可维护性都比较差。举个例子,比如在tableView头部要显示一段文字。我见过的最啰嗦的解决方案是这样的:

  1. 子类化一个UIViewController
  2. 将根View设置成一个UIScrollView
  3. 把头部的Label和TableView加在ScrollView上面
  4. 开始各种调整ScrollView和TableView的delegate调用函数里面的参数,让Label能随着TableView滑动

其实如果你熟悉UITableView,那么你几句话就可以搞定

所谓工欲善其事必先利器,编程语言和各种库其实本质上就是工具而已。你要想用这些工具来实现产品和Leader提出的各种需求。当然,不止是功能上的实现,还包括程序效率,代码质量。特别想着重强调一下代码质量,如果你不想后面维护自己的代码就像噩梦一样,如果你不想一旦新来一个需求就得对代码大刀阔斧的伤筋动骨,如果你不想给后来者埋坑。那么最好就多注意一下。

这里的代码质量并不是简简单单的指代码写点注释了,利用Xcode提供的一些像pragam或者#warning来解释代码。《编写可阅读代码的艺术》还有其他一些编程的书籍也都说道,真正高质量的代码,是不需要注释的。一个好的代码从逻辑上和结构上都是清晰的。我看到很多很难维护的代码都是因为逻辑结构混乱,和设计模式滥用导致的程序结构紊乱。分析其原因,就会发现很多时候,是因为写代码的人对所使用的工具(主要是objc和UIKit)不是非常熟悉,于是就写了很多凑出来的临时方案,简单的实现了功能。表面看起来挺好的,但是实际上代码已经外强中,骨子里都乱了。后期维护起来会让人痛不欲生。

同时,个人一直觉得对于搞IOS开发来说自己实现一遍TableView就像是一种成人礼一样。你能够通过实现一个UITableView来深入的理解UIKit的一些技术细节,对IOS UI编程所使用到的工具,有比较深入的了解。这样,写程序的时候才不会捉襟见肘。

言归正传。开始实现一个TableView。

UIKit给我们提供的基础

又重复了一遍,工欲善其事必先利其器。那么我们就看一下UIKit为我们提供了那些好用的工具让我们来实现一个TableView(当然不是子类化一个UITableView了)。

几何布局框架

《核心动画编程》的某个翻译版本把UIKit的布局模型翻译成了几何布局模型,这个词非常贴切,原始的英文是“struts and springs”。字面翻译就是结构和弹簧。其实说白了就是一种绝对布局模型,这种布局模型的核心数据就是一个对象的几何属性。所以翻译成几何布局模型还是比较贴切的。

在UIKit的几何布局模型中核心的一个数据结构是:CGRect,它确定了一个View(或者Layer,我们这里先只考虑View的情况,想不详细展开来说其他的)在父View中坐标系的绝对位置。

那让我们来看一下CGRect的定义:

我们发现其实一个CGRect中包含了一个原点(point)和一组宽高的信息(size)。其实一个CGRect就是描述了一个长方形的块,就像下图的红色方块一样的东西,我们的每一个View在坐标系中都会被表示为一个长方形的块状物。

比如我们有一个位置是{{10,10},{20,20}}的View:

在它的父类的坐标系中展示如下图:

Unnamed QQ Screenshot20140302084626

我们能够发现红色的View的frame信息所描述的几何位置,其实是其在父View坐标系中的绝对位置。死死的写在那里的。所以像UIKit这样的布局模型又叫绝对布局模型,如果你用过jave的Swing或者c++的QT,你可能会觉得这种绝对布局模型好麻烦,好啰嗦。没有布局管理器的概念,什么都是绝对的。但是只能说各有各的好处把。QT之类的有布局管理器的开发复杂界面的确方便,但是像在iphone这样的手机设备上,机器屏幕有限、设备性能有限,用绝对布局模型还是比较合适。苹果在IOS5之后也引入了一些相对布局的东西(autolayout)正好这里有篇文章是说其性能的Auto Layout Performance on iOS。读过之后你能发现自动布局在复杂界面情况下的性能的确比较差的。所以像UIKit这种比较原始的绝对布局在性能上还是有优势的。

扯回来,通过上图我们能够发现,UIKit的坐标系是一个二维平面坐标系,以左上角为原点,x轴横向扩展,y轴纵向向下扩展。y轴的防线可能和我们以前上学的时候,学的坐标系有点不太一样。这个估计是考虑在ios屏幕上布局的时候我们一般都是从上往下布局,y轴向下方便我们布局吧。既然知道了UIKit的坐标系统是一个二维平面坐标系统,那么我们以前学的很多几何知识就能够在这个坐标系统中尽情使用了。这里知识点太多不一而足,也是埋个伏笔,知道我们在些TableView的时候会用到很多几何上的知识。

同时,你可以把整个UIKit的View布局系统看成一个递归的系统,一个view在父view中布局,父view又在其父view中布局,最后直到在UIWindow上布局。这样递归的布局开来,就能构建起我们看到的app的界面。

UIView相关函数

通用的一些函数

先来说一些UIView的函数,我们着重讲一下和布局相关的,本着做一个TableView的目的嘛,先熟悉我们要用的,其他的读者慢慢看文档。

1、 初始化函数?- (id)initWithFrame:(CGRect)aRect

objc构建一个对象使用的是两段式,首先分配内存alloc然后init,这样的好处就是将内存操作和初始化操作解耦合,让我们能够在初始化的时候对对象做一些必要的操作。这是个很好的思路,我们在做很多事情的时候都可以使用这种两段式的思路。比如布局一个UIView,我们可以分成两部,初始化必要的子view和变量,然后在合适的时机进行布局。

而这个两段式的第一步就是:

这个函数是无论你用什么初始化函数都会被调用的一个,比如你用[UIView new]或者[[UIView alloc] init]都会调用initWithFrame这个函数(有些UIView的子类有特殊情况,比如UITableViewCell,怀疑apple对其做过特殊处理),所以你要是对一个view的变量有初始化的操作尽量往initWithFrame里面放还是非常合适的。 这样能够保证,以后在使用的时候所有的变量都被正确的初始化过。而我们一般会在initWithFrame中做些什么呢?

  1. 添加子View
  2. 初始化属性变量
  3. 其他一些共用操作

所以我们一般会看到这样的代码

在初花的时候将一些共用的初始化操作独立成一个函数commomInit然后再其中做上面说的事情,这样做的好处就是将初始化的代码集中到一起,如果你在实现的一个其他的什么initWithXXX的时候,直接调用commonInit就可以了。

不得不说的是,千万不要被这个函数的名称withFrame给忽悠了,以为这个函数使用布局用的。在代码逻辑比较清晰的工程中,几乎很少看到在这个函数中进行界面布局的工作。因为UIKit给你提供了一个专门的函数layoutSubViews来干这个事情。而且,在这个函数中做的界面布局的工作,是一次性编码,你的界面布局没有任何复用性,如果父View的大小变了之后,这个View还是傻傻的保持原来的模样。同时也会造成,初始化函数臃肿,导致维护上的困难。

2、layoutSubviewssetNeedsLayout

上面说了一些initWithFrame的事情,告诫了千万不要在里面做界面布局的事情,那应该在什么地方做呢?

就是这个地方,这是苹果提供给你专门做界面布局的函数。

我们来看一下文档:

The default implementation of this method does nothing on iOS 5.1 and earlier. Otherwise, the default implementation uses any constraints you have set to determine the size and position of any subviews. Subclasses can override this method as needed to perform more precise layout of their subviews.

You should override this method only if the autoresizing and constraint-based behaviors of the subviews do not offer the behavior you want. You can use your implementation to set the frame rectangles of your subviews directly.

You should not call this method directly. If you want to force a layout update, call the setNeedsLayout method instead to do so prior to the next drawing update. If you want to update the layout of your views immediately, call the layoutIfNeeded method.

苹果都说了这个是子类化View的时候布局用的。那我们最好是老老实实的在里面做布局的工作。

如何布局

这是个比较有意思的话题,因为可能很多人认为很简单,绝对布局嘛就是写一些死数字嘛,直接写CGRectMake(10,10,20,20)这样的坐标不就行了。如果你真这样认为,那么下面的话可能对你有帮助。

首先,尽量不要在布局的时候直接写死数字,比较稳妥的变法是使用常亮或者宏定义,甚至你定义一个临时变量也都ok,这样代码的可维护性就会变得比较好。

其次,谁说绝对布局的框架不能写成相对布局的方式。Apple提供了一个CGGeometry.h的文件,里面定义了大量的方便几何布局的函数。比如CGRectGetMaxX用来获取一个View的最大x坐标。你可能会问这有什么用?我们来看段代码:

下面那个textLabel的布局就是在imageView的大小而确定的。这不就是一些布局管理器做的事情吗,这不就是相对布局的概念嘛。所以我们完全可以使用UIKit的几何坐标系统完成一些相对布局的事情,而且也推荐这样做。

什么时候布局

这个就看功能需要了,不过有一点是肯定的就是不要直接调用layoutSubviews函数。UIKit和runtime是捆绑很密切的,apple为了防止界面重新布局过于频繁,所以只在runloop合适的实际来