我开发了一个 SwiftUI 库,将 CSS 引入 iOS 开发

阅读数:1127 2019 年 9 月 26 日 15:54

我开发了一个SwiftUI库,将CSS引入iOS开发

在 APP 开发中,快速实现效果至关重要,而样式的可复用、易维护可以帮助开发人员做到这一点。本文作者开发的 Swift-CSS 正是这样一个实现样式系统的库,它将 CSS 的技术优势引入 SwiftUI 开发中,不仅可以实现 SwiftUI 里样式属性的复用、解构,还能变化出许多类似 Web 领域的优秀技术方案。通过了解它的应用方法,或许会对你的日常工作有所助益。

本文的主角 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 有基本的了解)

什么是样式系统?

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

我开发了一个SwiftUI库,将CSS引入iOS开发

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

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 就代表了设计师对 主标题 的视觉要求,可以在后面的界面中反复使用,而不需要字体、字号、字重、颜色再定义一遍。

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

我开发了一个SwiftUI库,将CSS引入iOS开发
我开发了一个SwiftUI库,将CSS引入iOS开发

看起来需要定义的样式还有点多,但得益于 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 样式。

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

1. 使用样式系统,要求视觉和开发同学对整体视觉有全局掌握。对于视觉同学,梳理视觉规范,定义哪些是通用规则,哪些是个性规则,哪些是基础规则,以及如何对基础规则进行运算;开发同学提供样式接口时,需要在实现视觉要求的基础上,还能够保证扩展性和易读性。在对视觉规范有深入理解之后,设计出来的视觉规范才有用,更健壮。

2. 作为页面仔,在日常工作中,快速实现效果是非常重要的,希望我们的样式:

  • 可复用。如果视觉稿是按照原有规范实现的,那么新需求里的页面,也可以使用已有的样式来快速搭建,就像搭积木一样。

  • 易维护。而且实际工作中,在某个具体页面迭代最多的恐怕就是视觉优化了。如果你使用的样式系统,在处理二行变三行、按钮右上角加个图标、整个文字描述块整体向右移动等等需求变化时,如果能够快速实现,而不是需要结构大改(这样容易改出新问题),那么说明你的样式系统和 UI 接口划分是面向需求变化的。能够应付大部分(不要求 100 %)需求增改,就是个设计良好的组件。

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 是不是更见文知意,更易用呢?

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 = @" 请确认以下信息 ";

简单的对照,发现复用只能复用属性,如举例中的 YXColorGray4 和 NYQCode_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 里的样式系统。

SwiftUI

SwiftUI 里的链式语法,是函数式函数调用的体现。SwiftUI 实体分为 View 和 ContentModifier。Text(“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)")

我开发了一个SwiftUI库,将CSS引入iOS开发

以 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。

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 带来的好处:

解耦

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

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

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

复用

当有视觉规范后,按照规范,在公用的样式文件里,预先定义好所有基础样式,如“主标题”文字样式等,然后定义若干公用的业务样式,如出错弹窗。理想情况下,业务样式和组件样式都可以像搭积木一样,由这些基础样式拼凑而成。

性能提升

按照理论,CSSStyle 这样的计算结果,是一种类似编译后的缓存(compiled code),总是有提升的。具体的测试数据,待 iPhone 11 上市和 macOS 10.15 发布之后再做评测。请关注 SwiftUI-CSS,后续会补充。

样式继承

在 CSS 领域,sass 提供的一些高级应用,如样式继承(见第三节 CSS 里的样式系统的例子),SwiftUI-CSS 也内置了:

复制代码
let fontStyle = CSSStyle([.font(.caption)])
let colorStyle = CSSStyle([.backgroundColor(.red)])
let finalStyle = fontStyle + colorStyle
button.addClassName(finalStyle)

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

更多想象空间

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

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 中得以实现!

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

后记

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

参考链接:
https://sass-lang.com
https://en.wikipedia.org/wiki/CSS_Zen_Garden

评论

发布