iOS架构设计(解耦的尝试)之UI样式复用与布局管理

抽象的威力

从小老师就耳提面命:要看透问题的表象看本质。起初不解,随着干Coder时间长了,愈发察觉要想偷懒就得去解决本质问题,而不是围绕着问题的表层转来转去。而进一步说,不能停留在使用简单抽象出来的概念解决问题的层次,应该进一步使用一些高级抽象。就拿布局这个事情来说吧。

原始抽象

第一层的抽象是对现实世界在数字世界的描述,用坐标系统。用数学语言来描述布局这个事情。其实抽象到这一步也是一个非常大的进步了。在iOS中也就是我们常用的spring&struct的布局系统。通过frame等来标注一个view来控制平面布局,通过View-Tree来控制Z轴方向上的布局。

基于这种抽象的情况下,我们做的事情就是精准的描述每一个View在坐标系统中的位置。这个就是那个繁重的工作。在layoutsubviews函数里面里面,各种计算大小和偏移量。熟悉iOS的同学,随意看一下自己View里面layoutSubviews函数的大小就可以感受到其繁重。

描述抽象

抽象这个东西有意思的地方就在于他是可以递归使用的。在第一层抽象得到的概念基础上,进行第二层抽象;在第二层抽象得到的概念基础上,进行第三层抽象…..如此反复。当然,Apple牛逼的工程师们也会意识到spring&struct的繁重,于是就有了Autolayout(自动布局系统)。

Autolayout是基于描述的抽象,更多的是描述元素与元素之间的位置关系。通过解n元一次方程组来确定元素的位置(在坐标系统中的frame)。而这种基于模式的抽象,要比刚才第一层的抽象要高级很多。在这种抽象概念下,我们发现我们写的布局的代码有了一个大幅度的减少。原先需要拼了老命计算view frame的情况变成了,描述view之间关系的过程。原先需要非常多代码才能完成的任务,现在只要简单的写一句AView在x轴方向上距离BView固定间隔为10就可以完成了。随着代码量的降低,我们的对于布局系统理解成本都在降低。

复杂性由固有复杂性和认知复杂性组成。

而基于描述的抽象,因为是对第一层抽象概念的操作,忽略了第一层抽象的很多细节,很大程度上降低了布局系统的认知复杂度。无论从代码量上还是我们的认知成本上,都让我们可以『偷懒』了。

任务抽象

而有了基于描述抽象的Autolayout之后,我们会发现其实很多任务还是很繁重,比如我们要写个List布局,不可能每次都把List中每个元素的位置关系描述一边啊。比如我们要写个Collection的布局,也不可能每次都把每个元素的位置关系都描述一边啊…..

于是真对某些特定任务,会使用任务抽象。把List布局抽象成UITableView,把Collection布局抽象成UICollectionView,把线性布局抽象成UIStackView。。。。。而且Apple的工程师们也会在这条路上越走越远。在近几个版本的iOS-SDK更新中,我们也看到了更多的这种布局View的出现。相信以后也会更多。

而这种类型的View在简化编程工作这件事情上要比autolayout来的更加厉害。可以随意感受一下,我原先为了实现一个TableView所写的代码量DZTableView。就知道当我们把通用任务抽象出来的时候,能偷多大的懒了。

而关于这种任务抽象我们听到的最多就是面试中经常被问及的GoF设计模式。在想想应用设计模式时的爽,也就知道抽象这个工具的确很好用。在这里强烈推荐一本书《元素模式》。个人认为这本提供了一整套的瑞士军刀,来帮你进行抽象或者去设计『设计模式』。

….

当然这里还会存在更高层次的抽象。不过嘛,抽象并非银弹。并不是说一味的抽象下去就能够得到极致的『偷懒』。随着抽象层次提高,概念密度也在提高,而那些在这个过程中所忽略的特殊场中的细节,将变得难以还原。有些时候,处理问题反而更加困难,编码量却在增加。老生常谈的问题啊:度。当抽象层次满足业务需求和业务发展的时候,就可以临时先止步了。

DZGeometryTools

github地址:https://github.com/yishuiliunian/DZGeometryTools
主要是用了描述抽象和任务抽象两种技术。看起来忽悠人,其实实现部分一看就明白了。无非是将常用的一些任务转换成了函数而已。
其实在CoreGraphics框架中又一个CGGeometry.h文件,其中提供了很多方便操作CGRect等几何类型的函数。而在DZGemetryTools中所做的工作可以看做是对CGGeometry的一个扩展。

DZGeometryTools.h中主要是对几何类型的操作

通过抽象我将比较常见的几何操作归结为以下几种比较基础的操作:
1.偏移操作
2.缩放操作
3.间距计算操作
4.margin施加操作
现在把他们提取出来,基本上就是完成了一个工具集的构建。而后在这些工具集上,就可以来描述每个Rect之间的关系,这个有点类似于autolayout,不过是提前收工算好了,而autolayout是自动计算并赋值的。贴段真实使用中代码来感受一下:

- (void) layoutSubviews
{
    [super layoutSubviews];
    CGSize imageSize = {62*2, 84};
    CGRect contentRect = CGRectCenterSubSize(self.bounds, CGSizeMake(20, 20));
    CGRect textRect;
    CGRect imageRect;
    
    CGRectDivide(contentRect, &textRect, &imageRect, 30, CGRectMaxYEdge);
    imageRect = CGRectCenter(imageRect, imageSize);
    
    CGRect imgRs[2];
    CGRectHorizontalSplit(imageRect, imgRs, 2, 0);
    _indicatorImageView.frame = imgRs[0];
    _powerImageView.frame = imgRs[1];
    _textLabel.frame = textRect;
    _backgroundView.frame = self.bounds;
    _lastTimeLabel.frame = imageRect;
}

其中使用到了CGRectCenterSubSize 来对contentRect做了margin计算,并用CGRectHorizontalSplit 对rect坐了纵向均分的操作,这些都是描述UI元素之间相对位置关系的关系,我们通过手工计算来完成了对于UI元素的布局操作。这些函数都没有采用直接操作view.frame的方式,只进行了几何运算,计算出了UI元素坐标。之所以采取这种方式,是因为想基于目前的抽象层次应该是针对于几何概念的操作。对于UI元素的操作,已经简化成了一个赋值操作,没有太大必要去优化处理了。而相较于以前的编码量和思维量来说,基于目前构建的这个工具集来进行编码已经省了不少力气了。体力活少了。

DZLayoutMacros.h 常用的任务

在这个文件里面依旧是一些常用的布局工具集,不过和上面不一样的是采用了另外的表达方式:宏。针对于一些比较简单的任务,做了抽象。比如:

1.y依赖于顶部元素,并且尽可能填充满width的布局
2.顶部固定高度,铺满width的布局
3.//底部固定高度,铺满width的布局
4.…..

StyleSheet

github地址:https://github.com/yishuiliunian/StyleSheet
据说一个终端开发人员将会有70%以上的时间在和UI打交道。自己想想也对,貌似有很大一部分时间花费在了调整UI样式,addSubView还有layout上面。猛然间就发现自己的代码中有大量这种东西存在

self.label.layer.cornerRadius = 3;
    self.label.textColor = [UIColor darkTextColor];
    self.label.font = [UIFont systemFontOfSize:13];
    self.label.backgroundColor = [UIColor greenColor];
    self.label.layer.borderWidth = 2;
    self.label.layer.borderColor = [UIColor redColor].CGColor;


    self.label2.layer.cornerRadius = 3;
    self.label2.textColor = [UIColor darkTextColor];
    self.label2.font = [UIFont systemFontOfSize:13];
    self.label2.backgroundColor = [UIColor greenColor];
    self.label2.layer.borderWidth = 2;
    self.label2.layer.borderColor = [UIColor redColor].CGColor;


    self.button.layer.cornerRadius = 3;
    self.button.backgroundColor = [UIColor greenColor];
    self.button.layer.borderWidth = 2;
    self.button.layer.borderColor = [UIColor redColor].CGColor;

    self.aView.layer.cornerRadius = 3;
    self.aView.backgroundColor = [UIColor greenColor];
    self.aView.layer.borderWidth = 2;
    self.aView.layer.borderColor = [UIColor redColor].CGColor;
    ......
    

上面的代码是为了实现这样的效果而写的代码。

很多几乎是一毛一样的代码,充斥着整个APP。自己花在这些样式调整上的时间也非常多。为了实现一个样式效果,需要配置各种各样的属性。而且很多界面中这些样式都是一样的。于是又是无数次的重复上面的工作。oy my god!时间啊,就这样流走了。做为一个懒人,就会发问有没有一种可以少写点代码的方式呢?你可以写一个子类嘛,但是会有类污染的问题,单纯为了一个公有样式,就创建个子类有点大材小用。那写一批样式渲染的函数呗,恩这个注意不错,但是细想一下工作量也不小,而且不通用。于是,花了几天的时间我写了StyleSheet这个库。为了的就是来简化UI样式的编码。

通过上述描述我们可以发现,原始的写UI样式的问题:

1.繁琐的代码,大量重复性的工作
2.样式无法共享,每一个View都需要重新进行样式赋值。

而StyleSheet的设计目标就是:

1.样式配置轻便化,能够使用更加少的代码来描述View的样式
2.样式在View之间的共享.不止是相同类的实例之间的共享,甚至是跨类的共享。
So,先看看上述代码使用StyleSheet之后的效果:

self.label.style = 
DZLabelStyleMake(
  style.backgroundColor = [UIColor greenColor];
  style.cornerRedius = 3;
  style.borderColor = [UIColor redColor];
  style.borderWidth = 2;
  style.textStyle.textColor = [UIColor darkTextColor];
  style.textStyle.font = [UIFont systemFontOfSize:13];
);
self.label2.style = self.label.style;
self.aView.style = self.label.style;
[self.button.style copyAttributesWithStyle:self.label.style];

设计

基础抽象模型很简单,就是要让界面上关于展示的属性可以被组合使用。而我们所谓的样式,其实也就是各种基础属性组合出来的结果。基于这个模型,在设计StyleSheet的时候故意淡化了被渲染的View的类型的概念,任何一种类型的Style可以对任何类型的View进行渲染,但是必须是这种类型的View支持Style所指称的属性。比如你可以使用真对Button设计的DZButtonStateStyle来渲染一个UILabel,但由于UILabel不支持DZButtonStateStyle中的渲染属性,所以渲染结果是无效的。

但是当使用DZButtonStyle(继承自DZViewStyle)来渲染UILabel的时候,会使用DZButtonStyle中其父类的某些渲染属性,来渲染UILabel的父类UIView所支持的那些属性。

使用

直接使用Style对View进行渲染:

DZLabelStyle* style =
DZLabelStyleMake(
    style.backgroundColor = [UIColor greenColor];
    style.cornerRedius = 3;
    style.borderColor = [UIColor redColor];
    style.borderWidth = 2;
    style.textStyle.textColor = [UIColor darkTextColor];
    style.textStyle.font = [UIFont systemFontOfSize:13];
);

[style decorateView:self.label];

直接渲染的好处是,不用再次生成Style对象,更加方便样式在多个View之间渲染。
赋值渲染
对UIKit中常用的一些组件进行了扩张为他们增利了style属性,直接进行style属性的赋值,会出发一次渲染操作。当第一次调用style属性的时候,会自动生成一个zeroStyle并赋值。

self.label.style = style

或者

self.label.style = DZLabelStyleMake( 
      style.backgroundColor = [UIColor greenColor]; 
      style.cornerRedius = 3;
      style.borderColor = [UIColor redColor]; 
      style.borderWidth = 2;                          
      style.textStyle.textColor = [UIColor darkTextColor];      
      style.textStyle.font = [UIFont systemFontOfSize:13]; 
); 

当进行赋值渲染的时候,会将Style的Copy后的实例与当前View绑定,当更改Style的属性的时候,对应View的样式会立刻改变。

通用样式的共享

使用原有的配置,进行通用样式的共享是个非常困难的事情,基本上都是体力活,靠人力来维护。我们的代码中会掺杂大量的用于配置样式的代码,而且是独立且散在。 现在你可以通过StyleSheet解决: 定义共享的样式:

//在头文件中使用 xxx.h 声明一个公有样式
EXTERN_SHARE_LABEL_STYLE(Content)

//在实现文件中使用 xxx.m ,实现一个公有样式
IMP_SHARE_LABEL_STYLE(Content,
   style.backgroundColor = [UIColor clearColor];
   style.cornerRedius = 2;
   style.textStyle.textColor = [UIColor redColor];
)

(1)使用共享样式,方式一 self.label.style = DZStyleContent();
(2)使用共享样式,方式二(推荐) 很多时候, 如果不需要进一步更改样式,可以不采复制赋值的方式来进行渲染,可以直接使用: [DZStyleContent() decorateView:self.label]; 只进行渲染,而不进行复制。
好了,现在可以尝试着换这种方式来写UI样式了。

BY:yishuiliunian

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,988评论 25 707
  • 问答题47 /72 常见浏览器兼容性问题与解决方案? 参考答案 (1)浏览器兼容问题一:不同浏览器的标签默认的外补...
    _Yfling阅读 13,747评论 1 92
  • 开心的时候去看《欲望都市》吧。伤心的时候也去看《欲望都市》吧。 大学的时候,这部剧正火,但是当时只匆匆看了一会,就...
    FAN心理阅读 599评论 1 2
  • 字符串转换整数需要注意下面四个问题,尤其需要注意关于溢出的处理。字符串是否为空是否包含正负号是否包含其它字符是否溢...
    鬼谷神奇阅读 298评论 0 0
  • 系辞上 第一章 天尊地卑,乾坤定矣。卑高以陈,贵贱位矣。动静有常,刚柔断矣。方以类聚,物以群分,吉凶生矣。在天成象...
    田园读书人阅读 853评论 4 8