聊聊UI标准化

文 | Siam & Alex

一、背景

有赞零售从17年8月出生到现在,在过去的一年多时间里,整个零售APP团队忙于为线下商家提供各种经营场景的解决方案,在这种“赶工”下,毫无疑问,欠下了太多的技术债。UI风格不一致,交互体验不统一无疑是被吐槽最多的一点。为了避免开发与设计师相恨相杀,UI标准化就这样自然而然的提上了日程。

二、解决思路

设计师吐槽设计稿还原度低,已有的东西无法复用;开发吐槽设计稿不统一,交互模式不一致。因此UI标准化是开发与设计的标准。两者相辅相成,接下来,我们来分别从“设计师的角度”和“开发的角度”来阐述UI标准化该如何去做。

2.1 设计师的角度

每个设计师都有自己的设计习惯,都会有属于自己的“素材库”,虽然产出的设计稿风格是大差不错,但是细节上由于没有足够的约束以及规范因此很难保证统一,导致开发同学拿到之后不能很好的复用。如果将设计师的“素材库”统一,是否就可以有效避免该问题呢?考虑到设计师设计稿使用的要是Sketch。而Sketch提供了

Text Styles

When designing interfaces or websites that contain a lot of text layers, many of those layers will contain the same text properties. In Sketch, you can define a Text Style to apply to these layers, so you can update their style with ease.

Layer Styles:

Layer Styles allow you to save a set of stylistic elements, that can be reused across any of the layers in the document you’re working in, or any other document with Libraries. Layer Styles allow you to make sure that the styles of similar layers are always kept consistent and up to date, if you make changes to their appearance.

Symbols:

Symbols is a powerful feature in Sketch that allows you to reuse elements easily across your document’s Artboards and Pages, or even multiple documents. A Symbol is made up of two parts: a “master”, which takes the appearance of an Artboard—and an “instance”, which is a flattened representation of the master.

从这就可以看出Styles可以理解成UI Appearance,而Symbolds就是一个个封装好的组件。因此可以封装好一个个的Styles(下文统称为Library),共享到每一个设计师,要求其使用同一套。这里,我们针对Styles提供了一整套的基础样式供选择使用,原则上不允许设计师私自新增/更新样式(若确实需要,需要设计师内部共同评审确定并同步到开发侧)。

2.2 开发的角度

左图是以前拿到的设计稿,右侧是采用了上文提到的Layer Styles标注的设计稿。对比一下,可以发现,对于坐标、圆角、背景色等可以很直观的看到,但是对于Borders,并不能直观的看到Border Color,左图和右图最主要的区别就是在Appearance这一栏,右侧的截图是采用了上文提到的Layer Styles。这里我们创建了一个名为Border_DSB4Layer Styles,属性信息如下:

border-color: #1989FA;  
border-radius: 2;  
border-width: 1;  
background-color: #FFFFFF;  

开发维护一套与Library一一对应的样式配置代码,开发过程中只需要处理这个Style即可(是不是有点类似于换肤?)。

思路有了,那就开整,毕竟实践是检验真理的唯一标准

三、UI标准化实践

设计师很快就提供了他们认为的UI标准化需要做的内容(如图),整体上分为两部分:Style与各类自定义标准组件。接下来着重介绍Style以及标准组件搭建。

3.1 Style实践

不管是Text Styles 还是 Layer Styles,其包含的属性N多。以上文的Border_DSB4为例,若某几个Layer只是radius不一样,不可能创建Border_DSB4_1Border_DSB4_2Border_DSB4_n,维护成本太高,得不偿失。同样的,对于开发同学来说,同样一个Layer,有可能是ButtonLabelViewImageView都有可能,因此也不可能为每一种实现方式都维护一套Library一一对应的样式配置代码。鉴于此,精简一下,我们只针对Color 以及 Font维护成 Library(其它的就由UI标准组件来做吧~)。

以Color为例:

Color Library (部分)

而标准组件内部只需要扩展出这些对应key的颜色即可:

iOS为例

#pragma mark - 深天蓝/DeepSkyBlue
+ (UIColor *)yz_DSB1Color;
+ (UIColor *)yz_DSB2Color;
+ (UIColor *)yz_DSB3Color;
+ (UIColor *)yz_DSB4Color;
+ (UIColor *)yz_DSB5Color;
+ (UIColor *)yz_DSB6Color;

Android系统上需要注意不同屏幕的色差,因此尽量使用对比度比较明显的颜色,而不是在不同的屏幕上使用不同的颜色,降低维护成本。

3.2 组件实践

虽然UI组件的本质就是封装一个个自定义View,但是考虑到项目中实际情况,需要解决如下几个问题:

  • 有些组件是Pad或者Phone独有的
  • Phone和Pad都有的组件,但是有些细节:圆角、边框粗细、字号等不一样
  • 业务方拿到就可以直接使用,不需要进行任何额外的参数设置
  • Phone和Pad用的图片等资源文件会不一样
  • 在相同平台上的不同APP主色系不一样,使用的切图资源不一样

组件,自然而然就想到了cocoapods subspec。整体的组件架构如下:

平台如果没有特殊说明,指的是Phone或者Pad,而不是操作系统(iOS/Android)

3.2.1 Core

上层标准组件的核心所在,所有的自定义组件均可以在这里找到,提供了各种可配置属性供上层使用,内部不做任何个性化设置,与业务完全剥离

示例如下:

可以自由设置文本的内边距:

/**
 常规使用的Label,可以自定义文本的padding,以期达到更好的展示效果
 */
@interface YZLabel : UILabel

/**
 默认:(0, 0, 0, 0)
 */
@property (nonatomic, assign) UIEdgeInsets contentEdgeInsets UI_APPEARANCE_SELECTOR;

@end

按钮点击回调通过Block、便捷设置图文布局、背景色等

/**
 常规按钮,会有一些默认配置
 contentEdgetInsets:默认是(10, 10, 10, 10)
 */
@interface YZButton : UIButton

@property (nonatomic, assign) YZButtonImagePosition imagePosition;  //图片和文字的展示方式,默认是left
@property (nonatomic, assign) CGFloat spacingBetweenImageAndTitle;          //图标与文字之间的间距,默认是0

/**
 按钮的点击事件:UIControlEventTouchUpInside
 */
@property (nonatomic, copy, nullable) YZButtonAction buttonAction;

/**
 设定按钮文字和图片的展示方式

 @param imagePosition 展示方式
 @return N/A
 */
+ (instancetype)buttonWithImagePosition:(YZButtonImagePosition)imagePosition;

/**
 设置不同状态下的背景色

 @param fillColor 背景色
 @param state state
 */
- (void)setBackgroundColor:(UIColor * _Nullable)fillColor forState:(UIControlState)state;

@end

3.2.2 Common

如图所示,该层是在Core层基础之上,定位是为业务方服务,因此对几乎所有组件都提供了针对性的工厂方法,内部针对UI标准完成所有样式的配置与处理,业务方只需要简单创建即可使用,例如如下的按钮:

一个按钮包含normalhighlighteddisabled。而业务方使用就比较简单了,示例:

YZIndicatorButton *normalButton = [YZIndicatorButton yz_buttonWithButtonType:YZButtonTypeDarkGhost size:YZButtonSizeNormal];  
//按钮禁用
normalButton.enabled = NO;  
//设置loading状态下的文本
normalButton.indicatorMessage = @"Loading...";  
//开始动画
[normalButton startAnimating];
//停止动画
[normalButton stopAnimating];

这样业务方只需要关心业务逻辑,在UI上就不需要关心不同状态下的UI样式配置。

注意(敲黑板)

因为该层是提供给业务方使用,根据UI标准化的约定,业务方不得随意更改样式,因此,该层需要尽可能的减少自定义属性的暴露。

3.2.3 展现层(Pad&Phone)

这一层,我们是以平台(Pad or Phone)维度进行拆分,而不是产品维度,具体原因下面会有说明。

对于大部分App来说,到了Common层就可以达到要求了,但是由于零售Pad和零售Phone在UI上是完全独立的两款App,零售Pad并不是零售Phone的简单放大版本,因此零售Pad并不能简单的复用Phone的交互设计,所以在Common层的基础上又扩展出了两个独立的subspec,该层的主要作用是针对Common层组件在不同平台的样式做个性化配置以及该平台独有组件维护。如上图所示,每一个subspec都是由两个部分组成:

  • Appearance(样式配置)
    比如我们常用的Toast,在PhonePad上的背景、字号、圆角均有细微的差别,因此需要一些小小的改动,iOS里主要借助了UIAppearance(非UIKit的可以自己实现一套类似的协议)。
        YZNotifyHUD *appearance = [YZNotifyHUD appearance];
        appearance.textFont = [UIFont yz_regularFontWithFontStyle:YZRetailStyleFontT5];
        appearance.bezelCornerRadius = 5;
        appearance.bezelViewColor = [[UIColor yz_colorWithStyleName:YZUIStyleColorN8] colorWithAlphaComponent:0.88];
        appearance.textIconVerticalSpace = 8;
        appearance.contentEdgetInsets = UIEdgeInsetsMake(9, 12, 9, 12);
        appearance.margin = UIEdgeInsetsMake(40, 40, 40, 40);
  • Custom Kit(独有组件)
    比如虚拟键盘,在Phone里并不需要,因此可以将其直接放到HD里作为其独有的标准组件。

3.2.4 Resource(动态资源配置)

上面三层已经可以完整应对同一款产品,或者采用同一设计风格(包括主色)的产品的日常业务开发,但是如果**同一平台的不同产品采用的主色不一样的话**, 例如: 由于产品主色不同,导致单选色系不一样,组件库里无法有效支持,基于这种情况,我们提出了两个方案:
  1. 展现层调整为以产品维度进行拆分;
  2. 抽离出针对该产品的资源配置文件。
对于方案一缺点很明显:
  • 展现层每一次变更都需要考虑到每一个product subspec
  • subspec个数与接入的产品数正相关
  • 新产品初期接入成本较高,需要每一个组件都过一遍

而对于方案二,其实可以参考业界比较成熟的换肤。综合考虑,我们选择了方案二:

  1. 维护比较轻松
  2. 新增产品的时候,不需要改动源代码,只需要新增一套配置文件即可

还有最重要的一点,它可以脚本实现,直接从设计稿转~(虽然还没开始着手去弄)。

如上所述,展现层是以平台维度进行区分,Resource则是以产品维度进行划分,完全独立

这里有一条红线,尽可能少的让业务方去设置这些属性信息(比如上面的例子,不可能让业务方去设置这些图标),这个口子一放开,后续将一发不可收拾,标准化也就失去了其作用,沦为了简单的组件。

四、交付

交付之后,成效如何,业务方说了算。最终的交付产物如下: - UI标准组件库 - UI标准组件库API文档 - UI标注组件设计语言规范

出乎意料首先被吐槽的居然是最基础的Color Style。 业务方反馈使用非常困难,虽然设计师已经按照要求定义了Color Library,但是有些颜色,并不能在sketch的Appearance中描述(例如2.2里的Fills),由于禁止了业务方直接使用RGB,业务方还得查看色板才能找到对应的Key。鉴于这一点,UI标准组件提供了code template,即iOS里的code snippets,Android里的快捷键

以iOS的颜色代码块为例 为每一个颜色都提供了两个代码块:通过颜色Key以及RGB值都可以快速完成颜色的使用

然后就是设计稿么有遵守规范(削他)、组件不全等。

五、成果

在整个标准化过程中,不管是设计语言小组全程参与,还是标准组件架构的设计,都充分考虑了多APP的使用场景,因此标准组件(设计标准&组件标准)是多APP通用(有赞通用),因此为大幅提升开发与设计的效率提供了保证。

以UI标准化在零售团队中的落地为例:

  • 有赞零售的颜色顺利从几百种锐减到四十种(包括报表需要的二十种)
  • 业务开发更多关注于逻辑,提升开发效率
  • UI还原度高,设计师验收效率提升
  • 设计稿规范统一
  • 零售App交互保证统一,弥补了以前很多遗漏的细节
  • ....

六、踩坑

本质就是封装自定义组件,so easy的事情。做下来才发现too young too simple

  • 返工、返工、返工

    几乎发生在每一个组件的完成过程中。场景遗漏、两端不统一、和设计语言规范不统一。总而言之,沟通不够。

  • UI标准化≠UI组件

    前者是在产品层面保证设计与交互统一,我们要做的是标准化,因此在满足业务的基础之上,尽可能少的减少可配置属性的暴露,比如上文提到的单选按钮,虽然也可以暴露API由业务方设置图标,但是这样就违背了标准化的理念。

  • 与设计语言小伙伴多多交流

    标准化初期只有一个设计师在配合,从0到1,很多规范都是在摸索过程中尝试出来,由于一开始主要是以开发的角度来设计,出现了不符合设计语言的规范,例如色板的code定义。

  • 增量交付,小步快跑。敏捷!敏捷!敏捷

    通过早期和连续型的高价值工作交付满足“客户”。有段时间采用的是项目的形式,经常会出现业务方想用我们已经开发完成但是还未发布的组件,结果等组件发布之后,业务方还得进行组件替换;或者批量发布多个组件,组件开发人员要同时支持N多个组件的接入。因此建议增量交付的方式,开发验收完成一个组件,就可以发布出去供业务方使用,及时获得业务方的反馈,也有利于后期其它组件的开发。

  • 组件替换

    需要充分考虑影响面。是按照业务维度,还是组件维度,结合实际情况来考虑。例如按钮这种基础组件的替换,就不能一次性换掉,建议穿插在其它业务开发项目中,而对于颜色的替换,整个工程内上万处,因此在某个夜深人静的时候,脚本批量替换。

  • 组件迭代

    组件的迭代分为两种,新增以及更新。组件更新优先由原负责人跟进,对于新增组件,可以加在storyboard上,由有兴趣的小伙伴主动认领。组件迭代需求更多是来源于业务开发中,因此建议在业务需求技术评审的时候就需要评估是否需要组件支持(更新 or 新增),若需要,是直接在此次项目中完成还是单独立项,由PO来决定。

  • 组件开发评估

    工作量评估的时候,千万千万别只当做自定义组件来评估。需要考虑尽可能多的业务场景。对于复杂的业务组件,需要尽可能的做好抽象,便于业务方使用。组件评估的时候,建议明确该组件需要提供的能力,保证统一。如果当前组件有可能会暴露给WeexFlutter等跨平台框架,则需要考虑各端API的统一。一个组件的开发可能只有一个人,但是评估的时候可采用敏捷扑克德尔菲类比等等。

七、后续

UI标准化现在只是迈出了一小步,距离标准与自动化还有很长的一段路要走。不仅仅是组件的完善,还有就是尽可能的减少纯体力的工作。
纯体力的显而易见要借助于自动化。这个时候就不得不提一下Sketch了。 sketch提供了很多非常有用的工具以及扩展,尤以其中的sketchtoolSketch Plugin

sketchtool

解析设计稿

sketchtool dump /path/to/sketchfile > /path/to/jsonfile  

解析结果示例

{
  "<class>" : "MSDocumentData",
  "assets" : {
    "<class>" : "MSAssetCollection",
    "colors" : [],
  "foreignSymbols" : [
    {
      "<class>" : "MSForeignSymbol",
      "libraryID" : "96EC723F-3A3F-46A2-B4BA-28851151B133",
      "objectID" : "60FE5904-DBE7-4D3E-B5F1-4203F3E7E0D3",
      "originalMaster" : {
        "<class>" : "MSSymbolMaster",
        "backgroundColor" : {
          "<class>" : "MSColor",
          "value" : "#FFFFFF"
        },
        "booleanOperation" : 0,
        "clippingMaskMode" : 0,
        "exportOptions" : {
          "<class>" : "MSExportOptions",
          "exportFormats" : [],
          "includedLayerIds" : [],
          "layerOptions" : 0,
          "shouldTrim" : 0
        },
        "frame" : {
          "<class>" : "MSRect",
          "constrainProportions" : 0,
          "height" : 214,
          "width" : 1542,
          "x" : 385,
          "y" : -10733
        },
        "hasBackgroundColor" : 0,
        "hasClickThrough" : 1,
        "hasClippingMask" : 0,
        "horizontalRulerData" : {
          "<class>" : "MSRulerData",
          "base" : 0,
          "guides" : []
        },
        "includeBackgroundColorInExport" : 1,
        "includeBackgroundColorInInstance" : 0,
        "includeInCloudUpload" : 1,
        "isFixedToViewport" : 0,
        "isFlippedHorizontal" : 0,
        "isFlippedVertical" : 0,
        "isFlowHome" : 0,
        "isLocked" : 0,
        "isVisible" : 1,
        "layerListExpandedType" : 1,
        "layers" : [
          {
            "<class>" : "MSLayerGroup",
            "booleanOperation" : 0,
            "clippingMaskMode" : 0,
            "exportOptions" : {
              "<class>" : "MSExportOptions",
              "layerOptions" : 0,
              "objectID" : "AF1F809C-D623-4933-BB1A-5006A5C5814D",
              "shouldTrim" : 0
            },
            "frame" : {
            },
            ........more

拿到这些东西,该怎么用,就仁者见仁智者见智了,导出图片,显然也是支持的。

sketch plugin

基于CocoaScript,具体可参照 Sketch Developer

基于如上的两个小工具,希望后续可以考虑

  • 配置文件动态化
  • Sketch To Code
  • 独乐乐不如众乐乐
欢迎关注我们的公众号