模块化TableView

什么是模块化TableView?

举个例子

现在有需求让你修改一个类似于微博个人中心的界面,这种界面几乎所有内容都是写死的界面,虽然我估计微博的个人中心是完全动态的,但这里先假设是写死的,况且模块化也适用于动态界面

最常见的写法

普通的tableView,对应不同的cell:

image

建立个普通数组,把不同内容对应的模型丢进去,如model内容:

struct TableModel_1 {

    var icon:UIImage?

    var title:String = ""

    var desc:String = ""

}

struct TableModel_2 {

    var min:Double = 0
    var max:Double = 0
    var current:Double = 0

}
struct TableModel_3 {

    var desc:String = ""

}

struct TableModel_4 {

    var icon:UIImage?

    var title:String = ""
    
    var backgroundImage:UIImage?

}

然后到cellForRow方法中获取不同的model,分开获取cell,设置cell:

if let cell = cell as? RegularCell{
    cell.titleLabel.text = node.title
    cell.descriptionLabel.text = node.desc
    cell.iconImageView.image = node.icon
}
if let cell = cell as? RangeCell{
    cell.titleLabel.text = node.title
    cell.maxLabel.text = "\(node.max)"
    cell.minLabel.text = "\(node.min)"
    cell.sliderView.value = Float(node.current)
}
if let cell = cell as? DescriptionCell{
    cell.descriptionLabel.text = node.desc
}
if let cell = cell as? BlurCell{
    cell.titleLabel.text = node.title
    cell.iconImageView.image = node.icon
    cell.backgroundImageVIew.image = node.backgroundImage
}

再到didSelectRow中根据indexPath对不同的cell点击事件做不同的处理

let pushedCtr = PushedCtr()

switch indexPath.section {
case 0:
    switch indexPath.row {
    case 0: pushedCtr.title = "2018新闻"
    case 1: pushedCtr.title = "今日温度"
    default: break
    }
case 1:
    switch indexPath.row {
    case 0: pushedCtr.title = "广告位招租"
    case 1: pushedCtr.title = "今日湿度"
    case 2: pushedCtr.title = "谷歌回归"
    default: break
    }
default:break
}

self.navigationController?.pushViewController(pushedCtr, animated: true)

有的人可能会建多个数组,然后分开存放标题和图片的内容,这一步优化这里就跳过直接写成model

成品:

image

仔细一看,这里可以做的优化不是一般的多

首先cellForRow中,设置cell的工作可以交给各自的cell处理,在获取cell后只要对cell设置model就行了

但这样子的话cell和model的耦合就太高了,一般会再建一层viewModel来实现cell的设置, 由于不是重点这里就先不做

接着model其实不用分那么多种,所有类型的数据都可以放在一起,不同的类型分开放在一起,后期需要改动也相对方便一些

struct TableNode {
    var type:Int
    
    //type == 0
    var icon:UIImage?
    var title:String = ""
    var desc:String = ""

    //type == 1
    var min:Double = 0
    var max:Double = 0
    var current:Double = 0
    //title

    //type == 2
    //desc


    //type == 3
    //title
    //icon
    var backgroundImage:UIImage?
}

偏题一下,由于ObjC类的底层实现,编译后的二进制包会保存所有类各自的信息,不像C++具有zero-cost abstraction ,编译后的类信息只有偏移量.所以创建的ObjC类越多,二进制包就会越大,虽然增加的大小一般可以忽略不计,但前期稍微注意一下可以推迟后期可能会遇到下载包达到150M的问题

既然把model合并了,可以通过访问者模式为不同类型的Cell编写不同的初始化方法区分类型,后期需要添加新的Cell值需要一行代码就知道设置什么参数,为了区分,修改后成为node

struct TableNode {
    var type:Int
    
    var icon:UIImage?
    var title:String = ""
    var desc:String = ""
    init(regularWithIcon icon:UIImage?, title:String , description:String) {
        self.type = 0
        ...
    }
    var min:Double = 0
    var max:Double = 0
    var current:Double = 0
    init(rangeWithTitle title:String , min:Double , max:Double,current:Double) {
        self.type = 1
        ...
    }

    init(description:String) {
        self.type = 2
        self.desc = description
    }

    var backgroundImage:UIImage?
    init(webWithTitle title:String , backgroundImage:UIImage , icon:UIImage) {
        self.type = 3
        ...
    }
}

规范化可以通过适配器模式给Cell创建协议,协议中只有一行属性model

protocol TableNodeProtocol {

    var node:TableNode?{get set}

}

let node = nodeList[indexPath.section][indexPath.row]
var cell = tableView.dequeueReusableCell(withIdentifier: "\(node.type)") as? (UITableViewCell & TableNodeProtocol)
cell?.node = node

可以把cell的点击事件也可以通过策略模式放到node中,到时候didSelectRow就能把操作和indexPath的绑定转型成操作和model绑定,后期怎么改都不需要在意这里

struct TableNode {

    var type:Int

    var icon:UIImage?

    var title:String = ""

    var desc:String = ""

    var selectCell:(()->())?

    init(regularWithIcon icon:UIImage?, title:String , description:String,selectCell:@escaping ()->()) {

        self.type = 0

        ...

        self.selectCell = selectCell

    }

    var min:Double = 0

    var max:Double = 0

    var current:Double = 0

    init(rangeWithTitle title:String , min:Double , max:Double,current:Double,selectCell:@escaping ()->()) {

        self.type = 1

        ...

        self.selectCell = selectCell

    }

    init(description:String,selectCell:@escaping ()->()) {

        self.type = 2

        self.desc = description

        self.selectCell = selectCell

    }

    var backgroundImage:UIImage?

    init(webWithTitle title:String , backgroundImage:UIImage , icon:UIImage,selectCell:@escaping ()->()) {

        self.type = 3

        ...

        self.selectCell = selectCell

    }
}

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    table.deselectRow(at: indexPath, animated: true)
    nodeList[indexPath.section][indexPath.row].selectCell?()
}

做完上面几步,你会发现今天的主题已经实现了,现在已经把tableView内容模块化了,不管是改动顺序也好,改动cell内容也好,改动点击cell的操作也好,都能在一个地方处理,不需要根据indexPath调整内容(或者创建一堆的枚举用于区分) ,既符合接口隔离原则,也符合迪米特法则,添加内容时只需:

subList.append(TableNode(regularWithIcon: #imageLiteral(resourceName: "news"), title: "2018新闻", description: "点击查看更多"){ [weak self] in
    let pushedCtr = PushedCtr()
    pushedCtr.title = "2018新闻"
    self?.navigationController?.pushViewController(pushedCtr, animated: true)
})

除了添加新的Cell类型,后续匹配工作都可以忽略不管

完整实现的例子可以在github上查看:

https://github.com/miku1958/ModularTable

2018.8.28更新,聊一下解耦

上述工程其实也没有好到哪去。

其一是耦合度太高

即使以现代APP cell重用率其实很低来说,为了结构的健全度着想,一旦所有东西都揉杂在一起,修改起来会麻烦很多,毕竟实际工程会比这个复杂好十几倍,辛辛苦苦模块化后,却因为耦合太高导致白费力气了也不好

其实个人并不是那么喜欢解耦合这个过程,因为解耦合就意味着要引进一大堆的中间类来隔离view和model,引入的类越多,以后这个模块被废弃后要删除无用文件就会更加麻烦,为了解决这个问题在解耦合的时候就需要花费更多的心思去设计

抱怨结束接下来就来说一下怎么对上面的工程解耦合

首先是上文一概而过的分离cell和model,最简单的办法就是引入一个viewModel来接管cell拿到model后设置内容,以RegularCell为例(其实就是把代码挪个位置):

  1. 创建一个新的viewModel:
struct RegularCellConfigurator {
    static func config(cell:RegularCell ,TableNode node:TableNode ) -> () {
        cell.titleLabel.text = node.title
        cell.descriptionLabel.text = node.desc
        cell.iconImageView.image = node.icon
    }
}
  1. 移除TableNodeProtocol
  1. 把cellForRow中的cell?.node = node替换成:
if let cell = cell as? RegularCell{
    RegularCellConfigurator.config(cell: cell, TableNode: node)
}

到此为止cell和node就基本分离了,剩下的无非是cell的创建和node.type是绑定的,可以把type改写成一个枚举,对node.type进行switch case来分离cell和node的绑定。

假设以后有新的地方需要用到这个cell,只需要在RegularCellConfigurator新增一个config方法即可和新的数据对接

还有就是node的复杂度,按照这个设想,目前有4种样式大概用了54行代码,假设实际项目会比这个复杂10倍,也就有可能到500行左右(当然model类不大可能这么大),可以按照内容拆分node,比如现在regular和description很像,就把两者拿出来作为node的子类或者用protocol分离开来,推荐用protocol来处理(可以少一个类),或者全部分离然后各自继承同一个protocol也行,但就会分的比较散,需要重复更多的代码,具体看规模决定,没有必要一定要分的特别细

其二优化一下node的创建方式

经过和同僚的讨论,如果把node的创建写成类似于:

addSection:{
    addNode:{
    
    }
}

的方式,调整tableview分组的时候就更加方便了

其三是我忘记聊动态化了

这个以后有机会的详细聊一下,不然写下来的篇幅比本文还长了哈哈哈

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,094评论 1 32
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,359评论 8 265
  • 一、简介 <<UITableView(或简单地说,表视图)的一个实例是用于显示和编辑分层列出的信息的一种手段 <<...
    无邪8阅读 10,596评论 3 3
  • 2017.02.22 可以练习,每当这个时候,脑袋就犯困,我这脑袋真是神奇呀,一说让你做事情,你就犯困,你可不要太...
    Carden阅读 1,339评论 0 1
  • 概述在iOS开发中UITableView可以说是使用最广泛的控件,我们平时使用的软件中到处都可以看到它的影子,类似...
    liudhkk阅读 9,025评论 3 38