iOS 架构

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

2017-02-16  本文已影响139人  阿凡提说AI

抽象的威力

从小老师就耳提面命:要看透问题的表象看本质。起初不解,随着干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

上一篇下一篇

猜你喜欢

热点阅读