iOS中UITableView和自定义UITableViewCe

作者: ac3 | 来源:发表于2016-04-05 21:51 被阅读25514次

    本文的重点并不仅是UITableView的基本使用方法,而是强调有关UITableView和UITableViewCell开发过程中的一些具体细节问题。基本信息请参阅Apple开发文档《Table View Programming Guide for iOS》

    概述

    Table可能是最擅长于展示数据的一种UI部件。因此UITableView这个类是iOS App开发中除button和Label之外最常用的控件类型。几乎任何一个App都离不开tableView。使用UITableView很简单,核心就是要实现两个protocol。这是由于tableView在MVC中只是V这一环,所以它需要额外的支持提供另外的MC两个环节的功能才能让一个table完整的工作。而这种支持实现的途径就是protocol,Apple要求开发者提供实现** UITableViewDataSource** 和UITableViewDelegate 这两个protocol的支持类来协同对应的UITableView的工作。一般情况下,实现delegate protocol的类是UITableViewController,而data source protocol 可以是controller负责,也可以是其他helper类完成。但更多情况下,我更倾向于单独的modal wrapper类来完成。现在很多的建议data source尽可能不要放在controller中,这样有利于解决mass controller的问题。可以参考objc.io这篇文章:《Lighter View Controllers》

    鉴于UITableView是如此的重要,iOS替我们定制化了两种类型的tableView:static和dynamic。前者适用于表格内容相对固定的场景,更多的使用在诸如Setting panel,Detail panel的地方;而后者适用于表格内容不固定,行数动态变化的场景,更多的使用在网络请求返回后,将动态的数据进行内容展示等。虽然Apple内置了几种(确切的说到目前为止是4种)table的style,但是对于App开发而言,大多数情况下都需要自行定制table对数据的展示方式,因此相对应的,table cell的定制化成为App开发的必修课题。

    那么下文将介绍一下Static table的使用中需要注意的一些地方和dynamic table中cell开发的方法总结。

    关于Static Table的报错

    static table的设置方法很简单,在Xcode中设置UITableView的类型为static即可。
    这里着重强调一个问题,绝大多数使用static table view的人都会遇到xcode报出的一个莫名其妙的错误:

    error.png

    而这个问题的原因是:放置static table的View Controller <u> ** 必须为iOS内置的UITableViewController类型 ** </u>
    据说这是xcode的一个bug,但是到目前为止还没有被“修复”的迹象,总而言之,造成的结果就是,如果你想在某个页面放置一个static table view,你必须单独放在UITableViewController中,而且仅仅手动将自己的viewController的类继承自UITableViewController或者在xib中强制改成UITableViewController都不行,必须是原生的UITableViewController。可是很多情况下我们确实需要在自己的viewcontroller中添加一个static table view。怎么办?解决方法是使用Container View

    1. 在你自己的ViewController中拖入一个Container View
    2. 删除这个Container View自动创建的segue和对应的target view controller


      containerView.png
    3. 拖入一个新的UITableViewController,加入一个table view,修改类型为static;
    4. Ctrl-drag container view到这个UITableViewController,在弹出的segue类型中选择Embed:


      statictable.gif

    当然,这种方式只适用于storyboard操作并且要求支持Container VC的iOS版本。在其他情况下,可以直接使用addSubView:,将UITableViewController的view(当然就是tableView)加到自己的“container”view之下。

    UITableViewCell的使用方法

    下文的重点是总结UITableView和UITableViewCell的核心方法。更多详情可以参考Apple的官方文档。

    1. 预定义Cell

    iOS自定义了4种常见的Cell格式,在UITableViewCell.h中的注释中Apple给了一些明确的提示这些预置的style一般都适用于什么场景:

    typedef enum {
      UITableViewCellStyleDefault,  // Simple cell with text label and optional image view (behavior of UITableViewCell in iPhoneOS 2.x)
      UITableViewCellStyleValue1,  // Left aligned label on left and right aligned label on right with blue text (Used in Settings)
      UITableViewCellStyleValue2,  // Right aligned label on left with blue text and left aligned label on right (Used in Phone/Contacts)
      UITableViewCellStyleSubtitle  // Left aligned label on top and left aligned label on bottom with gray text (Used in iPod).
    } UITableViewCellStyle
    

    4种类型在Xcode中对应的选项为:Basic, Right Detail, Left Detail和Subtitle。在iOS8下测试,对应的示例图如下:

    • Basic:


      Basic.png
    • Right Detail有图时:


      rightdetail1.png
    • Right Detail无图时:


      rightdetail2.png
    • Left Detail:


      leftdetail.png
    • Subtitle:


      subtitle.png

    在这4种格式都统一有3个property可以供你使用:

    • textLabel:一个主标题
    • detailTextLabel:一个副标题
    • imageView:一张详情缩略图片

    实际上这些预置类型就是把这3种元素做了些取舍然后在不同的位置组合了一下。我觉得在设计App的时候,在任何情况下,设计师和工程师都应当首先考虑这些预置的类型能不能满足需求,除非有足够的必要,否则不要轻易的浪费经历在重复构造定制化的View上。个人觉得Subtile模式已经适用于绝大多数对UI要求不高的场合。你完全可以自己修改这3个控件的一些属性来对UI进行微调。比如你可以尝试将imageView的大小放大一些。

    2. 自定义Cell

    UITableView是通过调用UITableViewDataSource中的tableView:cellForRowAtIndexPath:方法来获取每一行所需要的Cell的,所以绝大多自定义Cell的处理过程都是在这一步完成,另外由于UITableViewCell本身也是一个UIView,所以你也可以在UITableViewDelegate中的tableView:willDisplayCell:forRowAtIndexPath: 方法中对Cell的view做最后的定制化。

    注意: Apple明确指出,在tableView:willDisplayCell:forRowAtIndexPath:中应当只修改“state-based properties”,比如selection和background color等等,但是不应该是内容(content),也就是不要在这里进行任何数据处理。

    2.1 自定义Cell的创建

    有以下几种方法:

    • 方法1:使用Code继承预定义的cell样式,然后再手动添加自己的view

    这是Apple官方文档中的示例,它通过调用initWithStyle:reuseIdentifier:使用UITableViewCellStyleDefault参数来创建一个Cell,为了方便期间,我对代码稍做了些改动,一个是简化了数据加载,另一个是用NSTextAlignment替换了废弃的UITextAlignment:

      #define MAINLABEL_TAG 1
      #define SECONDLABEL_TAG 2
      #define PHOTO_TAG 3
      
      - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
            static NSString *CellIdentifier = @"ImageOnRightCell";
            static int i = 0;
            UILabel *mainLabel, *secondLabel;
            UIImageView *photo;
    
            UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    
            if (cell == nil) {
                  cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
                  cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
    
                  mainLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, 220.0, 15.0)];
                  mainLabel.tag = MAINLABEL_TAG;
                  mainLabel.font = [UIFont systemFontOfSize:14.0];
                  mainLabel.textAlignment = NSTextAlignmentRight;
                  mainLabel.textColor = [UIColor blackColor];
                  mainLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleHeight;
                  [cell.contentView addSubview:mainLabel];
      
                  secondLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 20.0, 220.0, 25.0)];
                  secondLabel.tag = SECONDLABEL_TAG;
                  secondLabel.font = [UIFont systemFontOfSize:12.0];
                  secondLabel.textAlignment = NSTextAlignmentRight;
                  secondLabel.textColor = [UIColor darkGrayColor];
    
                  secondLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleHeight;
                  [cell.contentView addSubview:secondLabel];
      
                  photo = [[UIImageView alloc] initWithFrame:CGRectMake(225.0, 0.0, 80.0, 45.0)];
                  photo.tag = PHOTO_TAG;
                  photo.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleHeight;
                  [cell.contentView addSubview:photo];
            } else {
                  mainLabel = (UILabel *)[cell.contentView viewWithTag:MAINLABEL_TAG];
                  secondLabel = (UILabel *)[cell.contentView viewWithTag:SECONDLABEL_TAG];
                  photo = (UIImageView *)[cell.contentView viewWithTag:PHOTO_TAG];
            }
    
            mainLabel.text = [NSString stringWithFormat:@"Title_%d", i];
            secondLabel.text = [NSString stringWithFormat:@"Description_%d", i];
            i++;
            NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"jpg"];
            photo.image = theImage;
    
            return cell;
      }
    

    最终效果如下:


    CustomCell1.png

    这里有两个重点:

    1. Cell的重用机制
      重用机制对于UITableView而言是非常重要的概念,这能够显著提高滑动TableView时的性能。如果不使用重用,那么每次新的Cell出现在屏幕上时都需要新创建一个,这对快速滑动而言是非常影响用户体验,并且会占用系统大量的内存开销。iOS的TableView使用的重用机制在原理上很简单,就是系统自动维护一个queue,这个队列放着一定数量的准备好的Cell UI object,滑出屏幕范围之外一定“距离”的Cell都会被回收到这个queue里,给马上要滑入屏幕范围内的Cell复用。
      使用重用的方法核心是两个概念:Cell Identity和dequeue。前者是标识一种具体的Cell的id,后者是将Cell从复用队列中取出的实际操作。首先你需要用这个id告诉系统你将要用于重用的Cell是谁(注册),然后你使用这个id从重用的队列里取出来(复用)。具体到本例中的API就是如下两个:initWithStyle:reuseIdentifier:dequeueReusableCellWithIdentifier:。后面将看到,对于不同情况下创建的Cell,注册和复用的调用也并不相同。

    2. 必须把自定义的控件添加在cell的contentView上,而不是view上,也就是:

       cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
       ...
       // Correct!
       [cell.contentView addSubview:yourCustomComponetView];
       // Wrong!
       // [cell.view addSubview:yourCustomComponentView];
      

    再次强调一点,Cell的Content View不是自己的root view。关于Cell View的结构可以参考这篇文章:《制作一个可以滑动操作的 Table View Cell》
    这里只摘出最终结论,能够让你对一个Cell的view有直观的理解。
    对于一个如图所示的Table而言:

    sample.png
    它的TableViewCell的View层级结构是:
    <UITableViewCell; frame = (0 396; 320 44);> //1
       | <UITableViewCellScrollView; frame = (0 0; 320 44); > //2
       |    | <UIButton; frame = (302 16; 8 12.5)> //3
       |    |    | <UIImageView; frame = (0 0; 8 12.5);> //4
       |    | <UITableViewCellContentView; frame = (0 0; 287 44);> //5
       |    |    | <UILabel; frame = (15 0; 270 43);> //6
    

    这个Cell 里有六个视图:
    * UITableViewCell 这是最高层的视图。 Frame 显示它有 320 点宽和 44 点高——宽度和高度都和预期的一致,因为它和屏幕一样宽,而高度就是 44 点。
    * UITableViewCellScrollView 虽然你不能直接使用这个私有类,但它的名字很好地暗示了它的功能。它的 Size 和 Cell 的一样。
    * UIButton 它在 Cell 的最右边,就是 Disclosure Indicator 按钮。
    * UIImageView 是上面 UIButton 的子视图,装载着 Disclosure Indicator 的图像。
    * UITableViewCellContentView 另外一个私有类,它包含 Cell 的内容。这个类对于开发者来说就是 UITableViewCell 的 contentView 属性。但它只作为一个 UIView 来暴露在外,这就意味着你只在其上调用使用公开的 UIView 方法;而不能使用任何与这个类关联的任何私有方法。
    * UILabel 显示 “Item #” 文本。

    很显然,这里cell.contentView并不是cell.view。

    • 方法2:使用xib绘制Custom Cell View

    这是自定义Cell最常见也是最省力的方法:

    1. 首先在xcode中创建custom xib,拖拽需要的控件到xib上,并做好布局和限制;
      2)新建自己的custom class,定义对应的outlet properties;
      3)将xib的class设置成对应的custom class,然后将outlet和控件连接起来;
      4)在适当的位置(一般为初始化的地方)调用registerNib:forCellReuseIdentifier:注册Cell;
      5)在tableView:cellForRowAtIndexPath:中调用dequeueReusableCellWithIdentifier:forIndexPath:复用cell

    下面的这个示例演示了上述步骤。在这个示例中,首先创建Custom Cell和xib的界面,其中有一张大图,下面有两个独立的label,然后我使用了tableView:willDisplayCell:forRowAtIndexPath:对每一行Cell的背景颜色做出最终设置,而在tableView cellForRowAtIndexPath:中对Cell的内容进行填充:

    Custom View:

    @interface MyTableCellView : UITableViewCell
    @property (weak, nonatomic) IBOutlet UIImageView *imageView;
    @property (weak, nonatomic) IBOutlet UILabel *name;
    @property (weak, nonatomic) IBOutlet UILabel *date;
    @end
    

    Custom Xib:

    CustomXib.png

    在table View Controller中:
    - (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup here ...
    ...
    // register the cell to tell the system preparing to reuse it.
    [self.tableView registerNib:[UINib nibWithNibName:@"TableCellView" bundle:nil] forCellReuseIdentifier:cellId];
    }

        - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
            // always reuse the cell
            MyTableCellView *cell = [tableView dequeueReusableCellWithIdentifier:cellId forIndexPath:indexPath];
            
            cell.frame = CGRectMake(0, 0, [[UIScreen mainScreen] bounds].size.width, 250);
            NSString *path = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"jpg"];
            cell.imageView.image = [UIImage imageWithContentsOfFile:path];
            NSDate *object = self.objects[indexPath.row];
            cell.date.text = [object description];
            return cell;
        }
    
        - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
        {
            NSLog(@"indexpath = %ld", indexPath.row);
            if ( indexPath.row % 2 == 0) {
              cell.backgroundColor = [UIColor redColor];
            }
            else {
              cell.backgroundColor = [UIColor greenColor];
            }
        }
    

    Demo:


    CustomCell2.png
    • 方法3:使用code定义一个Custom Cell Class

    这个在本质上和上一种使用xib的方法是一样的,只不过一个是用代码完全定制,一个是借助xib操作。但是在Cell重用的方式上有一定区别。上一种方法中,需要调用registerNib:forCellReuseIdentifier:来注册重用的Cell,在这里就需要用registerClass:forCellReuseIdentifier:方法来注册。与之对应的,获取重用的Cell的时候,调用dequeueReusableCellWithIdentifier:forIndexPath:方法。

    Class myClass = [MyTableCellView class]; 
    [self.tableView registerClass: myClass forCellReuseIdentifier:@"CustomCell"];
    ...
    ...
    MyTableCellView *cell = [self.tableView dequeueReusableCellWithIdentifier:@"CustomCell" forIndexPath:path];
    cell.label.text = @"text";
    ...
    

    注意 dequeueReusableCellWithIdentifier:dequeueReusableCellWithIdentifier:forIndexPath:的区别!!

    • 如果你注册过Cell,在没有可用的cell时,前者会返回nil;而后者永远都会从注册的nib或者class中替你创建一个可用的Cell。也就是说,前者调用你需要手动检查nil,而后者不需要;
    • 如果你从没有注册过cell,在没有可用的cell时,前者会返回nil,后者……直接崩溃!也就是说,调用后者你 必须确保注册过cell

    2.2 访问Cell中的控件:

    • xib中使用outlet
      这个应该不用多说,在Cell的原型中(不管是Static cell还是Dynamic cell)定义outlet properties,然后在xib中拖拽连接对应的控件即可;Apple官方文档上的示意图:
    connect_outlet.jpg connect_static_objects.jpg
    • 代码中使用viewWithTag:
      这个是获取parent view上某个特定view的快捷方法,首先需要设置一个sub view的tag,然后使用viewWithTag:来访问这个sub view。设置时可以通过xcode在xib中设置tag标签,也可以直接通过tag property手动设置:

    创建时:

    mainLabel = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, 220.0, 15.0)];
    mainLabel.tag = MAINLABEL_TAG;
    // customize the label here...
    [cell.contentView addSubview:mainLabel];
    

    访问时:

        mainLabel = (UILabel *)[cell.contentView viewWithTag:MAINLABEL_TAG];
    

    有关Table和Cell的性能需要注意的问题

    关于这个话题,Apple并没有用过多的篇幅介绍,但是在官方开发文档中明确提出了3点意见:

    1. 注意重用(Reuse Cell)
      这点我们已经在上文中着重强调过了。

    2. 避免反复的调整Cell的布局(Avoid relayout of content)
      只在创建每一个Cell的时候布局一次,而不要每一次获取Cell的时候都去重新布局。

    3. 避免透明的subviews (Use opaque subviews)
      自定义cell的时候,尽可能避免使用透明的控件,因为透明控件在table滑动时将增大渲染开销。

    总结

    本文主要从Static table和Dynamic table两方面总结了UITableView和UITableViewCell的核心使用方法和问题,并着重介绍了Custom Cell的几种方法和注意事项。

    另外,有兴趣的话,UITableView还有另外几个比较关键的功能可以继续研究,一个是Editing mode,一个是Table Index,还有一个相对也很重要的功能自定义Header和Footer View。有空的话可以再总结一下。希望此篇能帮助到您。

    2016年4月5日,完稿于南京。

    相关文章

      网友评论

      • FallLeaf:我出现过一种情况就是cell的个数不多,大概就三四个,cell里面的布局有textfield,在前面的textfield输入框输入数据并保存数据源后,滑动列表, 发现这条数据在另一个cell里面也显示了。能否帮我解答一下?
        ac3:@FallLeaf 如果你的datasource更新没有问题的话,多半是cell重用后没有给ui正确赋值更新的原因。检查dequeue之后的代码。具体的话要看代码。
      • 345530499b44:欢迎加入iOS技术攻城狮联盟,群号码:149615208 底层框架研究
      • singlestep:不知道作者有没有试过这个方法 prepareForReuse
        ac3:@singlestep 在collectionview cell里用过,原理都一样。
      • SwordAndTea:写的很细致,解决了我的问题
      • 416703ce99a4:写得很不错啊,,学习啦,mark,mark,
      • feng_dev:而且仅仅手动将自己的viewController的类继承自UITableViewController或者在xib中强制改成UITableViewController都不行,必须是原生的UITableViewController。

        这句的意思是必须创建的时候父类写的是 UITableViewController ,要是写了普通的控制器再手动改成 table Controller ,还是不行是吧,这种手动修改是只限于这一个例子吗,其他好像都行是吧
        ac3:@Coder_枫 只对static table有此限制。我想你自己写的baseTVC应该肯定是dynamic的,不然没多大意义吧
        feng_dev:@ac3 我写的 BaseTVC 是基本类,其他的都继承 这 base
        ac3:@Coder_枫 是的,至少目前的版本测试是这样
      • Bugfix:说得好啊,要是再加多点tableView上优化问题文章就完美了 :+1: ,,好文要顶~
        ac3:@JackMen 谢谢支持!关于性能问题,请关注我的graphic系列文章的相关更新。
      • 9a2e3c097760:在有很多控件的cell上,数据出现重影有没有什么好的办法解决呢?
        ac3:@singlestep 你说的有道理的,我之前没有想到这层。prepareForReuse确实能够处理重用的cell的清理问题,虽然Apple官方并不推荐在这里做任何和datasource content相关的清理动作,但是目前大家确实习惯在这里做些nil set的事情
        singlestep:估计是作者说的 "这种情况的出现就是因为/复用/,如果刚好某一个cell 没有显式的设置它的属性,那么它这些属性就直接复用别的cell" 可以试试 prepareForReuse 这个方法
        ac3:@倔强的少山 不是很明确“数据出现重影”具体指什么,我猜测你可能是说当Cell过于复杂的情况下,在快速滑动时UI出现的卡顿导致文字排版有些混乱的现象?如果是这种情况,建议你用Instrument检测一下GPU和CPU的占用率,如果是GPU占用率过高,建议将一部分图片渲染的操作用offScreen render做Cache;如果是CPU占用率过高,建议尽可能将绘制拿到异步线程里去做。

      本文标题:iOS中UITableView和自定义UITableViewCe

      本文链接:https://www.haomeiwen.com/subject/qglplttx.html