SwiftUI-CSS: 一个实现样式系统的库

本文的主角[SwiftUI-CSS](https://github.com/hite/SwiftUI-CSS) 是个 SwiftUI 库,它的目的是为了实现类似 web 开发领域结构样式分离的效果:

  • HTML 负责结构
  • CSS 负责结构样式

样式不写在 HTML 的属性里而是在 CSS 当中,不仅仅为了解耦;更重要的是复用,促使开发者把所有的业务样式需求分解,提炼良好的基础样式,以更系统方式的管理样式。

CSS 天然的提供 classname 机制,可以实现样式分组和组合;一个业务样式的最终效果可以是一些基础样式组合而成,不同组合呈现不同的效果。

<div class="fontStyle colorStyle floatStyle">
</div>

本质上讲 CSS 里的一个 classname 封装了一组属性(property)的集合,简称样式。多个 classname 即可组合成为一个样式系统;一个样式系统实现业务上组件设计。配合具体的 HTML 结构就是一个组件(component)。

SwiftUI-CSS 将 CSS 的技术优势带到了 SwiftUI 开发中,不仅可以实现 SwiftUI 里样式属性的复用、解构,还可以变化出很多类似 web 领域的优秀技术方案。SwiftUI-CSS 的详细使用可参见SwiftUI-CSS readme。本文试图探讨 SwiftUI-CSS 能为 SwiftUI,乃至 iOS 开发带来什么样促进和影响。

(阅读本文需要你对 SwiftUI 有基本的了解)

1. 什么是样式系统?

样式系统指的是对 UI 设计规范中,提炼出来的一些规范。以 Ant Design 为例。它的“字体使用规范”里指出,主标题的样式是这样的;

主标题

主标题的样式至少包含4 个关键属性:

  1. 字体 font family(包括英文字体)
  2. 字重 font weight
  3. 字号 font size
  4. 字体颜色 color
  5. 行高行间距(当文字有可能多行时)line-height

如果用 CSS 那么它的样式定义是这样的(以 main_title 作为样式系统里的命名):

.main_title{
    color: rgb(102, 102, 102);
    font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", 微软雅黑, SimSun, sans-serif;
    font-size: 16px;
    font-weight: 500;
}

定义好之后,.main_title 就代表了设计师对主标题的视觉要求,可以在后面的界面中反复使用,而不需要再字体、字号、字重、颜色定义一遍。

这是比较基础的样式,稍微复杂点的例子是按钮,按钮的样式不仅包含字体的样式,还包括按钮的边距、圆角、背景色等属性。

文案样式

按钮样式

看起来需要定义的样式还有点多;但得益于 CSS 的层叠样式的特性,文字和按钮两部分的代码可以写到一个样式 .buttonStyle里。

      .buttonStyle{
            width: 212px;
            height: 40px;
            border-radius: 20px;
            background-color: #dd1a21;
            line-height: 40px;
            font-family: PingFang-SC-Bold;
            font-size: 16px;
            color: #FFFFFF;
            text-align: center;
        }

后续界面里需要这种确定按钮的时候,只需要引用 .buttonStyle 样式名就可以了。更好的例子可参考 Twitter 出品的 Bootstrap 来学习如何组织管理 CSS 样式。

2. 为什么样式系统 对 App 开发重要

  1. 使用样式系统,要求视觉和开发同学对整体视觉有全局掌握。
    对于视觉同学,梳理视觉规范,定义哪些是通用规则,哪些是个性规则,哪些是基础规则,以及如何对基础规则进行运算;开发同学提供样式接口时,需要在实现视觉要求的基础上,还能够保证扩展性和易读性。在对视觉规范有深入理解之后,设计出来的视觉规范才有用,更健壮。
  2. 作为页面仔,在日常工作中,快速实现效果是非常重要的,希望我们的样式:
  • 可复用。如果视觉稿是按照原有规范实现的,那么新需求里的页面,也可以使用已有的样式来快速搭建,就像搭积木一样。
  • 易维护。而且实际工作中,在某个具体页面迭代最多的恐怕就是视觉优化了。如果你使用的样式系统,在处理:二行变三行、按钮右上角加个图标、整个文字描述块整体向右移动等等需求变化时,如果能够快速实现,而不是需要结构大改(这样容易改出新问题),那么说明你的样式系统和 UI 接口划分是面向需求变化的。能够应付大部分(不要求 100 %)需求增改,就是个设计良好的组件。

3. CSS 里的样式系统

上述的 main_title,buttonStyle 是基础元素样式,在组件库里,会有一些基础元素样式、基础功能样式,一些复杂的组件需要用这些基础元素样式、基础功能样式组合而成。

/**元素样式**/
.w-seperator{
    height: 2px;
    width: 100%;
    backgroundColor: #ff00ff;
}
/**功能样式**/
.f-hide{
  display:none;
}
/**功能样式**/
.f-clear_both{
   clear:both;
}
// 请忽略这个样式的实际意义
<div class="w-seperator f-clear_both f-hide"></div>

这里w-seperator f-clear_both f-hide 即是这个分割线的样式名称。

这是原生 CSS 就支持的使用方式,还是比较粗放,w-seperator f-clear_both f-hide 并不是那么简洁。如借助预编译,还可以使用变量、继承等特性来简化 CSS 的定义工作。比方使用 sass

.w-seperator{
    height: 2px;
    width: 100%;
    backgroundColor: #ff00ff;
}
.f-hide{
  display:none;
}
.f-clear_both{
   clear:both;
}
.seperator_in_list{
   @extend .w-seperator;
   @extend .f-hide;
   @extend .f-clear_both;
}

这样.seperator_in_list这个名字就是我们在后面界面里可用的样式名,比起 CSS 是不是更见文知意,更易用呢?

4. iOS 开发里的样式系统

Cocoa touch 并没有提供样式系统的语法,有些开发者可能会自己封装一层,大部分封装都比较初级。比方说只对 App 里的按钮封装了工厂类;或者只对 Label 设置字号、字体、颜色做了封装,没有形成进一步封装。

  • 对按钮 Button 的封装;
// 黑色中空,中间是clear color
+ (instancetype)yx_BlackHollowClearButton {
    YXButton* button = [YXButton new];
    button.titleLabel.font = [UIFont systemOfSize:14];
    
    [button setTitleColor:YXColorGray4 forState:UIControlStateNormal];
    [button setTitleColor:YXColorWhite forState:UIControlStateHighlighted];
    [button setTitleColor:YXColorGray10 forState:UIControlStateDisabled];
    
    button.layer.borderWidth = YX_ONE_PIXEL;
    button.layer.borderColor = YXColorGray4;
    button.layer.cornerRadius = YXButtonCornerRadius;
    button.layer.masksToBounds = YES;
    return button;
}
  • 对 UILabel 的样式封装
    UILabel *label = [UILabel new];
    [NYQSpec setLabelStyle:label withNYQCode:NYQCode_18_blk_med];
    label.textAlignment = NSTextAlignmentCenter;
    label.text = @"请确认以下信息";

简单的对照,发现复用只能复用属性,如举例中的YXColorGray4NYQCode_18_blk_med,如果要设置一组属性需要再次设置,没有一个对象如 importantStyle 来代表颜色和字体等,使得下一个 button 可以直接设置importantStyle 的。

// 不存在这样的系统接口
UIStyle *importantStyle = [UIStyle styleWithColor: [UIColor redColor] font: YX_Button_Font];
// 确认按钮
UIButton *confirm = [UIButton new];
[confirm setStyle: importantStyle];
// 提示按钮
UIButton *prompt = [UIButton new];
[prompt setStyle: importantStyle];

我想原因就是 Cocoa touch 设计之初就没有考虑用对象来表示一组属性,没有设计样式系统的概念,导致在封装实现样式系统时比较困难。

补充提示

[button setStyle:] 这个接口其实可以使用category技术来实现,UIStyle 可以用自定义封装,只要 UIStyle 实现了接口,任何样式的属性都可以封装到一个 UIStyle 的实例中。这种方式和下面即将介绍 SwiftUI-CSS 的封装本质不同在于,UIStyle 里的属性不能运算,[button setStyle:]本质是把属性挂在一个全局变量下,然后遍历,在性能方面没有提升,充其量是一种语法糖。

UIStyle *importantStyle = [UIStyle styleWithColor: [UIColor redColor] font: YX_Button_Font];
// 一种 setStyle 内部实现
- (void)setStyle:(UIStyle *)style{
    if(style.font){
        self.titleLabel.font = style.font
    }
    if(style.color){
        [self setTitleColor:style.color forState:UIControlStateNormal];
    }
}
// 理想中的,目前无法实现
- (void)setStyle:(UIStyle *)style{
    if (style.computedStyle == nil) {
        [style compute];
    }
// computedStyle 包含了字体和颜色
    [self setFinalStyle:style.computedStyle];
}

理想中的 computedStyle 在真正使用到样式上,才对所有属性进行一次计算,这样在后续其他 button 设置时,直接使用计算结果,而不是再次使用遍历的方式去一一设置。属性计算带来的性能提升,类似在 JS 模板引擎中常用的字符串模板编译成 function 带来效果,甚至更高。

使用 storyboard 的界面开发

使用代码实现样式系统至少还可以使用全部变量、宏、函数封装来达到某种意义上的复用,维护。但是如果使用 storyboard 实现的界面,则需要面对更多的问题。
storyboard 在快速搭建单个界面时效率非常高。假设需要更新品牌色时,至少还可以用 asset catalog 来实现全局的颜色修改,但是涉及到如“主标题”字号修改时,则显得无能为力,只能一个一个 storyboard 去修改,更不要说一起修改多个属性的组合了。

storyboard 最多可以在小组件层复用,向上到 ViewController 粒度太多不容易复用;向下只能使用 xib 复用组件—— storyboard 不存在样式系统。

直到 SwiftUI 横空出世,把描述性界面开发体验带到 iOS,它的函数式语法和属性对象方式,使得可以用Swift-CSS 来实现 SwiftUI 里样式系统。

5. SwiftUI

SwiftUI 里的链式语法,是函数式函数调用的体现。SwiftUI 实体分为 ViewContentModifierText("g_kumar") 负责视图结构;.font(.title) 添加属性样式。简单的实例;

Text("g_kumar")
      .bold()
      .font(.title)
Text("Notifications: \(self.profile.prefersNotifications ? "On": "Off" )")
Text("Seasonal Photos: \(self.profile.seasonalPhoto.rawValue)")
Text("Goal Date: \(self.profile.goalDate, formatter: Self.goalFormat)")

预览

g_kumar文字组件为例,我们应用函数式编程里的运算规律-结合律推导一番:

  • Text("g_kumar") 用 v 表示
  • .bold() 用 cm1 表示
  • .font(.title) 用 cm2 表示
  • 最终组件是 C
C = v * cm1 * cm2
// =>
C = (v * cm1) * cm2
// =>
C = v *( cm1  * cm2)
// =>
cm = cm1 * cm2
C = v * cm
// 假设 v1 是另外一个 Text,则
C1 = v1 * cm

所以,上面公式里的 cm 代表了样式的计算结果,在这里是指字形和字号的运算结果。利用这个计算结果,在后面的样式设置 v1,v2 等视图时可以直接使用 cm 来设置样式。它带来的性能提升,取决于 Apple 对 cm 这个计算变量的内部优化程度。鉴于目前 SwiftUI 闭源,我们还无法得知这种优化带来多大的提升;退一步讲,将计算结果封装为一个变量,当 Apple 后续对 ContentModifier 计算进行优化后,调用者可透明的享受到优化提升。

以上就是属性运算的原理,所以有了 SwiftUI-CSS。

5. SwiftUI-CSS 的样式系统

SwiftUI 的原理很简单。就是使用CSSStyle 对象来封装样式对象,然后通过 addClassName 这个 modifier 来将样式插入函数运算中,和其他事件、通知、样式(.frame\ .resizable)一起无缝协作。以 SwiftUI-CSS example 工程为例;

// without SwiftUI-CSS
Image("image-swift")
                 .resizable()
                 .scaledToFit()
                 .frame(width:100, height:100)
                 .cornerRadius(10)
                 .padding(EdgeInsets(top: 10, leading: 0, bottom: 15, trailing: 0))
// with SwiftUI-CSS
let languageLogo_clsName = CSSStyle([
    .width(100),
    .height(100),
    .cornerRadius(10),
    .paddingTLBT(10, 0, 15,0)
])
Image("image-swift")
        .resizable()
        .scaledToFit()
        .addClassName(languageLogo_clsName)

其中,languageLogo_clsName就是 logo 的样式名,在页面其他 logo,可以直接复用这个样式。更多使用示例请查看SwiftUI-CSS example 工程。

总结下 SwiftUI-CSS 带来的好处:

  1. 解耦
    如同 web 领域开发那样,.html 、.css 文件是分开的。以产品详情为例,典型目录结构是:

-- ProductDetail
|----ProductDetailView.swift
|----ProductDetailStyle.swift

ProductDetailView.swift 负责构建界面的结构,里面只有 view 元素、事件逻辑、数据流等,保持简洁;而ProductDetailStyle.swift 里面是一些样式的定义。两个文件分离有助于 diff 、review 和和他人协作。

  1. 复用
    当有视觉规范后,按照规范,在公用的样式文件里,预先定义好所有基础样式,如“主标题”文字样式等,然后定义若干公用的业务样式,如出错弹窗。理想情况下,业务样式和组件样式都可以由这些基础样式像搭积木一样拼凑而成。
  2. 性能提升
    按照理论,CSSStyle 这样的计算结果,是一种类似编译后的缓存(compiled code)总是有提升的。具体的测试数据,待 iPhone 11 上市和 macOS 10.15 发布之后再做评测。请关注 SwiftUI-CSS 后续会补充。
  3. 样式继承
    在 CSS 领域,sass 提供的一些高级应用如样式继承(见第三节3. CSS 里的样式系统的例子),SwiftUI-CSS 也内置了;
let fontStyle = CSSStyle([.font(.caption)])
let colorStyle = CSSStyle([.backgroundColor(.red)])
        
let finalStyle = fontStyle + colorStyle

button.addClassName(finalStyle)

利用CSSStyle提供的+ 运算,将多个样式合并实现继承效果。

6. 更多想象空间

以上只是我个人实践中遇到的场景,在别人的手里可能还会迸发出不一样的火花,以下是我的一些构想:

SwiftUI zen-garden 计划

在 web 开发早期,人们对 CSS 在 web 开发中扮演的角色定位不是很清晰。在 2003年,由 Dave Shea 发起了 CSS zen garden 计划。这个网站提供一套固定的带样式名,但是没有样式实现的 .html 文件,然后参与者提供不同的 CSS 文件,来对相同的 HTML 结构进行 stylize,试图探索 CSS 对 HTML 结构可定制能力的极限。时至今日,已经有 218 个五花八门的设计位列 Design List 其中,很多充满想象力的设计让人叹为观止。

CSS zen garden 的成功,让开发者意识到 CSS 的无限可能性,同时也激励诸多其它语言尝试相同的项目。也同样影响到我,而 SwiftUI-CSS 提供了可能性;

  • 提供一套固定的编写了 View 结构的文件如 html.swift,带样式名但是没有设置属性。
  • 参与者提供对这些样式名的实现文件,如 style.swift,和 html.swift 一起生成不同的界面设计。
    让我们一起探索使用 SwiftUI 可定制能力的极限。

以上方案称 SwiftUI zen garden(待实施)。

设计师和程序员协作——storyboard 未尽的夙愿

xib(storyboard 前身)早在 iOS1.0 之前就被 Apple 用在 iOS 的开发工作流中:设计师用 Interface Builder 编写 xib 文件,之后程序员用 xcode 在 xib 的基础上继续编写事件、数据等,业务逻辑。但是因为 xib 变更后较难 diff 和 xib 并不是程序员使用 oc 语言,不能无缝复用,导致设计师和程序员分离开发的目的没有实现。

大部分设计师用 xib 完成的 App prototype,都不能直接让程序员继续开发。

更多时候 xib 的工程只是为了做 App 原型,程序员还需要按照 prototype,完全或者部分用代码重写。

有了 SwiftUI,设计师可以使用 SwiftUI 编写 prototype,验证完毕之后,程序员拿 SwiftUI 源码继续开发,因为都是 swift 文件啊;设计师后续的样式调整,可以直接修改 style.swift 文件,不需要和程序员去竞争 html.swift 文件使用权,避免冲突。
设计师和程序员无缝协作的大和谐,在 SwiftUI 中得到实现!

也许你还能想到更多用法,是不是?

7. 后记

SwiftUI-CSS 1 个月前就写好了,当我发布到 Twitter、Hacknews 等地方,邀请各位大V 宣传时,并没有激起多少浪花,我认为它的重要性被低估了,故作此文。

参考

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,034评论 1 32
  • 平安夜, 梧桐叶落花儿谢。 花儿谢, 小河低诉, 晚风凄切。 云遮月色千里雪, 遥听大漠平安乐。 平安乐, 青山原...
    春江细语阅读 346评论 1 13
  • 要相信: 所有的难过,难是难,总会过; 你只管努力,剩下的交给时间️。 撑下去,就有后来的一切!
    Jenner接纳阅读 501评论 0 0
  • 月这个周末真的很忙碌,但是很温暖。和亲人在一起的时光,满满的都是爱。 谢谢你如此阔绰的送给爸妈结婚纪念日礼物,送完...
    萍萍淡淡阅读 279评论 1 3
  • 去年年底我爸查出肝癌,去医院化疗了一次,由于化疗的副作用太大,再加上我和哥哥都在上大学,家里没有多少钱去化疗,...
    懂我不言阅读 144评论 0 1