iOS实录1:UITableView构建UI界面

[这是第一篇]

导语:UITableView是iOS项目中使用相关广泛的UI组件,本文主要从构建界面方案的实现TableViewCell之间的通信Native动态化页面的野望(挖坑)三部分讨论了如何使用TableView构建比较常见的UI界面。

一、概述

1、问题#####
  • 在iOS的开发中,有大量的UI工作,这些UI工作简言之就是在画“页面”,“页面”的复杂度有高有低,复杂度高的比如绘制直播画面(聊天列表、点赞飞花、送礼物、广告UI等),复杂度低的比如绘制一个关于页面,一个logo和几行文本就搞定了。

  • 而我们在实际工作中遇到的UI工作大部分是中等复杂度的页面,比如某某列表页、某某详情页,在绘制页面的同时,还需要响应页面上的操作。

  • 其实,很多页面都是可以利用UITableView来构建出来的。

2、优势#####

采用分而治之的办法,将一个整体页面划分成若干部分,每一个部分就相当一类Cell,其实这么做是有好处的,好处如下:

  • 提高部分UI组件的复用性。尤其在同一个App中,甚至一个App中的同一功能模块中,有些UI是相同的,是可以复用的。

  • 支持数据驱动UI的展示,在页面数据变化的情况下,UI也能发生对应的变化。

  • 在绘制同一个页面时候,有利于多人合作,每个人可以负责一个页面中不同的部分,且在开发过程中,不受其他人的工作进度影响。

3、解决方案
  • 封装TableViewController,凡是想使用UITableView构建页面的Controller都需要继承该类。

  • 支持UI组件响应的的处理。

  • 使用通知中心或其他方案来完成Cell之间的通信。

  • 支持UITableView的上拉加载和下拉刷新,一般列表页面比较需要这两个功能。

二、 构建界面方案的实现####

1、QSTableViewCell 和 QSTableViewCellModel
  • QSTableViewCell是对UITableViewCell的封装,而QSTableViewCellModel是对QSTableViewCell上数据和UI响应动作的描述。

  • QSTableViewCell定义了其子类需要实现的办法,如如数据更新布局,cellHeight处理,点击cell的处理等主要方法。

     #pragma mark - QSTableViewCell
    @interface QSTableViewCell : UITableViewCell
    
    #pragma mark - 子类需重写
    - (void)layoutWithModel:(id)model;
    
    + (CGFloat)cellHeightWithModel:(id)model;
    
    - (void)onTapCellAction;
    
    @end
    
  • QSTableViewCellModel定义了对应Cell类的类名、展示需要的数据, 以及响应UI操作点击的动作,我们将响应UI操作的动作封装在Block中。

    #pragma mark - QSTableViewCellModel
    @interface QSTableViewCellModel : NSObject
    
    @property (nonatomic,copy)NSString  *cellClassName;
    @property (nonatomic,copy)QSTableViewCellActionBlock tapCellBlock;
    @property (nonatomic,assign)CGFloat cellHeight;
    @property (nonatomic,strong)id userInfo;
    
    @end
    

说明:cellClassName属性保存对应的Cell类的类名,可以利用反射来实现cell的创建。

2、实现UITableView代理方法

选择在TableViewController中实现UITableView的UITableViewDelegate、UITableViewDataSource的代理方法。在TableViewController中有顶一个dataSource数组,这个数组中存放的各个Cell对应的CellModel,利用cellModel中的信息,完成Cell的绘制和响应处理。以cell的创建、cell高度获取为例。

  • 根据cellModel来实现heightForRowAtIndexPath代理

    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    
       QSTableViewCellModel *cellModel = [self.dataSource objectAtIndex:indexPath.section];
       return [self.cellFactory cellHeightForData:cellModel];
    }
    
  • 根据cellModel来完成cell的创建和更新

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    
         QSTableViewCellModel *cellModel = [self.dataSource objectAtIndex:indexPath.section];
         QSTableViewCell *cell = [self.cellFactory cellInstanceForData:cellModel];
         [cell layoutWithModel:cellModel];
         return cell;
      }
    

    说明:在cellForRowAtIndexPath:代理方法中实现了cell的创建、复用、数据更新,添加cell上的响应动作处理。

  • QSTableViewCellFactory的职责:负责cell高度的计算、创建和复用

    //QSTableViewCellFactory的定义
    @interface QSTableViewCellFactory : NSObject
    
    - (instancetype)initWithTableView:(UITableView *)tableView;
    
    - (CGFloat)cellHeightForData:(id)data;
    
    - (QSTableViewCell *)cellInstanceForData:(id)data;
    
    @end
    

说明:self.cellFactory就是QSTableViewCellFactory对象,达到解耦的目的,使得Controller中代码更简洁。

3、上拉加载和下拉刷新

利用MJRefresh来实现TableView的上拉加载和下拉刷新。

  • 设置上拉加载

    - (void)setupRefreshFooter{
    
         //上拉加载更多
        @weakify(self);
         MJRefreshAutoNormalFooter *footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:^{
              @strongify(self);
                  [self refreshDataWithStyle:QSRefreshTableViewDataStyleLoadMore];
          }];
    
          [footer setTitle:@"已显示全部"forState:MJRefreshStateNoMoreData];
    
          footer.refreshingTitleHidden = YES;
          self.tableView.mj_footer = footer;
          [footer.stateLabel setTextColor:[UIColor lightGrayColor]];
      }
    
  • 设置下拉刷新

      - (void)setupRefreshHeader{
    
          //下拉刷新
          @weakify(self);
          MJRefreshHeader *header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
             [self refreshDataWithStyle:QSRefreshTableViewDataStylePull];
          }];
          self.tableView.mj_header = header;
      }
    
  • 结束刷新

    - (void)endRefreshingWithMoreData:(BOOL)hasMoreData{
    
        if (self.needPullRefresh) {
            [self.tableView.mj_header endRefreshing];
        }
    
        if (self.needPullToLoadMore) {
            if (hasMoreData) {
                [self.tableView.mj_footer endRefreshing];
            }else{
                [self.tableView.mj_footer endRefreshingWithNoMoreData];
            }
        }
    }
    

总结: 使用UITableView构建页面,Controller继承自定义的TableViewController,使用Cell来定义视图,CellModel来定义视图上的内容和动作,然后将CellModel放入TableViewController的dataSource数组中,即可。

三、TableViewCell之间的通信####

在使用TableView组织页面的时候,有个非常值得考虑的问题,那就是TableViewCellCell之间的的通信。

1、概述#####
  • 采用分而治之的思路,将一个大的视图拆分成若干个TableViewCell。而在业务上,这几个TableViewCell视图是相互关联的。如点击某子视图,另一个子视图的数据展示发生变化。这就导致了需要选择方案去实现部分TableViewCell之间的通信。

  • 早期方案是选用了通知中心

  • 通知中心不仅可以在不同的UITableViewCell之间传递数据的,还降低了代码耦合。但是这也导致了TableViewCell中大量存在注册观察,移除观察这样的代码(移除的工作还非常重要)。最关键的是,通知中心传递的参数不可以灵活地使用model对象。

  • 后期方案是QSMessageCenter,这是从业务出发,实现的通知中心的替换方案。

  • QSMessageCenter主要实现了不同对象之间的数据传递;传递的参数可以是任何的对象,包括数据model,数组,字典等;注册后不需要手动移除; 使用简单和代码耦合低。

2、通信方案的使用#####

通信方案使用了QSMessageCenter,主要使用步骤如下:

  • 在观察者中注册,指定receiverKey

    [self registerMessageReceiverWithKey:receiverKey];
    
  • 发送消息,指定receiverKey,当注册方法和发送消息中的receiverKey相同,才可以把消息发送给观察者

    [self sendMessage:message messageId:messageId receiverKey:receiverKey];
    
  • 在观察者类中实现qsReceiveMessage:messageId:方法

    - (void)qsReceiveMessage:(id)message messageId:(NSString *)msgId{
        //可以根据msgId去做不同处理
    }
    

说明:QSMessageCenter的具体详情参考iOS实录7:iOS通知中心的替换方案

四、Demo展示####

  • 根据方案,定义两类Cell和CellModel,可以实现如下的列表页。
列表页效果图.png
  • 根据方案,复用列表页的一类Cell和CellModel,新增一类Cell和CellModel,可以实现如下的详情页。
详情页效果图

总结:这样的方案,为实现列表页、详情页省去了大量的时间,开发者只需要集中精力做好Cell的绘制、CellModel的定义即可。

五、Native动态化页面的野望(挖坑)

H5页面有个很大的优点,就是可以不用发版就可以更新页面内容;如果可以利用TableView实现动态化的Native页面,岂不是很完美的事情。然后,现实的需求和业务比较复杂多变,实现这样的方案,投入和产出比是多少,能不能达到目标,都不好判断。但是针对某些特性的场景,实现页面的动态化数据更新还是可以的。

1、数据描述界面的各个组成#####

使用的是JSON格式数据,由后台返回,如下所示(部分JSON数据):

{
   "body" :[{
      "type": 0, 
       "content": "http://xxxxxxxxxxxxxxxxxxxxx.jpg",
       "target":"appName://previewSingleImage"
    },
    {
        "type": 1, 
        "content": "H5页面有个很大的优点,就是可以不用发版就可以更新页面内容;如果可以利用TableView实现动态化的Native页面,岂不是很完美的事情....",
        "target":"appName://lookAllContent"
    },
    {
        "type": 2, 
        "content": "http://xxxxxxxxxxxxxxxxx.mp4",
        "target":"appName://playInFullScreen"
    }
    //其他略
  ]
//其他略
}

说明1:type定义的是内容的类型,如0是图片,1是文本,2是视频;content定义的是内容;tagert定义的是点击内容的行为;tagert值的是App中自己定义的协议,如appName://previewSingleImage表明点击图片Cell预览全图,appName://playInFullScreen是点击视频Cell,全屏播放视频。

2、动态化UI方案#####

有两种方案可以选:

1)在Native实现盛装不同数据类型的容器Cell,和Cell的响应处理行为。

说明:该方案没有深入下去,因为TableView处理的视图比较简单,而且还给后台带来比较大的负担(本来后台开发资源就紧张),后期考虑使用CollectionView来实现特定场景下较复杂的动态化界面。

2)将数据和展示需求拼接成H5代码,然后由WebView渲染出来,结合JS和WebViewJavascriptBridge处理Cell的响应处理行为。

说明:该方案是目前采纳的方法,一定程度上实现了页面的动态化,比直接加载H5页面快很多。但是也仅仅是适用于某些固定展示行为的模块,如文章详情页等。

3、 总结#####

End

  • 源码QSUseTableViewDemo

  • 我是一个iOS开发程序猿,至今(2017年7月)正式做iOS开发已有1年多点的光景。iOS实(践)录系列是自己的一点开发心得。这个系列是17年4月1日开始写的,不仅是总结自己开发中经验,也是对自己的鞭策。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 概述在iOS开发中UITableView可以说是使用最广泛的控件,我们平时使用的软件中到处都可以看到它的影子,类似...
    liudhkk阅读 8,985评论 3 38
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,121评论 29 470
  • 前言 由于最近两个多月,笔者正和小伙伴们忙于对公司新项目的开发,笔者主要负责项目整体架构的搭建以及功能模块的分工。...
    CoderMikeHe阅读 26,995评论 74 271
  • 妈妈老了,耳朵有些背了,记忆力也不大好,常常叫错我们兄弟俩的名字。解决办法是要常常让她喜悦地参与活动,用笑话刺激大...
    chchfo阅读 309评论 3 1
  • 我像每个人都一段第一次独自走夜路的历程,我也不例外,不同寻常,它像一次刻骨铭心的修行让我明白了很多。 那是...
    灰叔漫画阅读 944评论 6 10